Merge pull request #93 from github/shana/cloning

Implementing the cloning action for the Clone dialog and friends
This commit is contained in:
Phil Haack 2015-03-18 17:21:57 -07:00
Родитель aea23bf55d 8c2ead291a
Коммит fdf9f5068d
29 изменённых файлов: 1206 добавлений и 31 удалений

Двоичные данные
lib/Microsoft.TeamFoundation.Git.Controls.dll Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -23,6 +23,7 @@ namespace GitHub.Api
public const string GitHubUrl = "https://" + GitHubDotComHostName;
public const string GitHubDotComHostName = "github.com";
public const string GitHubGistHostName = "gist.github.com";
const string ProductName = "GitHub Extension for Visual Studio";
const string clientId = "";
const string clientSecret = "";
public static readonly Uri GitHubDotComUri = new Uri(GitHubUrl);
@ -86,7 +87,7 @@ namespace GitHub.Api
Scopes = useOldScopes
? oldAuthorizationScopes
: newAuthorizationScopes,
Note = "GitHub for Windows on " + GetMachineNameSafe()
Note = ProductName + " on " + GetMachineNameSafe()
};
var handler = twoFactorChallengeHander ?? TwoFactorChallengeHandler.HandleTwoFactorException;
@ -112,7 +113,7 @@ namespace GitHub.Api
Scopes = useOldScopes
? oldAuthorizationScopes
: newAuthorizationScopes,
Note = "GitHub for Windows on " + GetMachineNameSafe()
Note = ProductName + " on " + GetMachineNameSafe()
};
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
@ -191,7 +192,7 @@ namespace GitHub.Api
HasIssues = repository.HasIssues,
HasWiki = repository.HasWiki,
HasDownloads= repository.HasDownloads
};
};
}
static string GetMachineNameSafe()

Просмотреть файл

@ -0,0 +1,342 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Text;
namespace GitHub.Authentication.CredentialManagement
{
public class Credential : IDisposable
{
const int maxPasswordLengthInBytes = NativeMethods.CREDUI_MAX_PASSWORD_LENGTH * 2;
static readonly object _lockObject = new object();
bool _disposed;
static readonly SecurityPermission _unmanagedCodePermission;
CredentialType _type;
string _target;
SecureString _password;
string _username;
string _description;
DateTime _lastWriteTime;
PersistenceType _persistanceType;
static Credential()
{
lock (_lockObject)
{
_unmanagedCodePermission = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
}
}
public Credential()
: this(null)
{
}
public Credential(string username)
: this(username, null)
{
}
public Credential(string username, string password)
: this(username, password, null)
{
}
public Credential(string username, string password, string target)
: this(username, password, target, CredentialType.Generic)
{
}
public Credential(string username, string password, string target, CredentialType type)
{
Username = username;
Password = password;
Target = target;
Type = type;
PersistenceType = PersistenceType.Session;
_lastWriteTime = DateTime.MinValue;
}
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
~Credential()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
SecurePassword.Clear();
SecurePassword.Dispose();
}
}
_disposed = true;
}
private void CheckNotDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException("Credential object is already disposed.");
}
}
public string Username
{
get
{
CheckNotDisposed();
return _username;
}
set
{
CheckNotDisposed();
_username = value;
}
}
public string Password
{
get
{
return SecureStringHelper.CreateString(SecurePassword);
}
set
{
CheckNotDisposed();
SecurePassword = SecureStringHelper.CreateSecureString(string.IsNullOrEmpty(value) ? string.Empty : value);
}
}
public SecureString SecurePassword
{
get
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
return _password == null ? new SecureString() : _password.Copy();
}
set
{
CheckNotDisposed();
if (_password != null)
{
_password.Clear();
_password.Dispose();
}
_password = null == value ? new SecureString() : value.Copy();
}
}
public string Target
{
get
{
CheckNotDisposed();
return _target;
}
set
{
CheckNotDisposed();
_target = value;
}
}
public string Description
{
get
{
CheckNotDisposed();
return _description;
}
set
{
CheckNotDisposed();
_description = value;
}
}
public DateTime LastWriteTime
{
get
{
return LastWriteTimeUtc.ToLocalTime();
}
}
public DateTime LastWriteTimeUtc
{
get
{
CheckNotDisposed();
return _lastWriteTime;
}
private set { _lastWriteTime = value; }
}
public CredentialType Type
{
get
{
CheckNotDisposed();
return _type;
}
set
{
CheckNotDisposed();
_type = value;
}
}
public PersistenceType PersistenceType
{
get
{
CheckNotDisposed();
return _persistanceType;
}
set
{
CheckNotDisposed();
_persistanceType = value;
}
}
public bool Save()
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
byte[] passwordBytes = Encoding.Unicode.GetBytes(Password);
ValidatePasswordLength(passwordBytes);
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
credential.TargetName = Target;
credential.UserName = Username;
credential.CredentialBlob = Marshal.StringToCoTaskMemUni(Password);
credential.CredentialBlobSize = passwordBytes.Length;
credential.Comment = Description;
credential.Type = (int)Type;
credential.Persist = (int)PersistenceType;
bool result = NativeMethods.CredWrite(ref credential, 0);
if (!result)
{
return false;
}
LastWriteTimeUtc = DateTime.UtcNow;
return true;
}
public bool Save(byte[] passwordBytes)
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
ValidatePasswordLength(passwordBytes);
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
credential.TargetName = Target;
credential.UserName = Username;
IntPtr blob = Marshal.AllocCoTaskMem(passwordBytes.Length);
Marshal.Copy(passwordBytes, 0, blob, passwordBytes.Length);
credential.CredentialBlob = blob;
credential.CredentialBlobSize = passwordBytes.Length;
credential.Comment = Description;
credential.Type = (int)Type;
credential.Persist = (int)PersistenceType;
bool result = NativeMethods.CredWrite(ref credential, 0);
Marshal.FreeCoTaskMem(blob);
if (!result)
{
return false;
}
LastWriteTimeUtc = DateTime.UtcNow;
return true;
}
public bool Delete()
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException("Target must be specified to delete a credential.");
}
StringBuilder target = string.IsNullOrEmpty(Target) ? new StringBuilder() : new StringBuilder(Target);
bool result = NativeMethods.CredDelete(target, Type, 0);
return result;
}
public bool Load()
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
IntPtr credPointer;
bool result = NativeMethods.CredRead(Target, Type, 0, out credPointer);
if (!result)
{
return false;
}
using (NativeMethods.CriticalCredentialHandle credentialHandle = new NativeMethods.CriticalCredentialHandle(credPointer))
{
LoadInternal(credentialHandle.GetCredential());
}
return true;
}
public bool Exists()
{
CheckNotDisposed();
_unmanagedCodePermission.Demand();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException("Target must be specified to check existance of a credential.");
}
using (Credential existing = new Credential { Target = Target, Type = Type })
{
return existing.Load();
}
}
internal void LoadInternal(NativeMethods.CREDENTIAL credential)
{
Username = credential.UserName;
if (credential.CredentialBlobSize > 0)
{
Password = Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / 2);
}
Target = credential.TargetName;
Type = (CredentialType)credential.Type;
PersistenceType = (PersistenceType)credential.Persist;
Description = credential.Comment;
LastWriteTimeUtc = DateTime.FromFileTimeUtc(credential.LastWritten);
}
static void ValidatePasswordLength(byte[] passwordBytes)
{
if (passwordBytes.Length > maxPasswordLengthInBytes)
{
throw new ArgumentOutOfRangeException("The password has exceeded " + maxPasswordLengthInBytes + " bytes.");
}
}
}
}

Просмотреть файл

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
namespace GitHub.Authentication.CredentialManagement
{
public class CredentialSet : List<Credential>, IDisposable
{
bool _disposed;
public CredentialSet()
{
}
public CredentialSet(string target)
: this()
{
Guard.ArgumentNotEmptyString(target, "target");
Target = target;
}
public string Target { get; set; }
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
~CredentialSet()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (Count > 0)
{
ForEach(cred => cred.Dispose());
}
}
}
_disposed = true;
}
public CredentialSet Load()
{
LoadInternal();
return this;
}
private void LoadInternal()
{
uint count;
var pCredentials = IntPtr.Zero;
bool result = NativeMethods.CredEnumerateW(Target, 0, out count, out pCredentials);
if (!result)
{
var lastError = Marshal.GetLastWin32Error();
Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Win32Exception: {0}", new Win32Exception(lastError).ToString()));
return;
}
// Read in all of the pointers first
var ptrCredList = new IntPtr[count];
for (int i = 0; i < count; i++)
{
ptrCredList[i] = Marshal.ReadIntPtr(pCredentials, IntPtr.Size * i);
}
// Now let's go through all of the pointers in the list
// and create our Credential object(s)
var credentialHandles =
ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
var existingCredentials = credentialHandles
.Select(handle => handle.GetCredential())
.Select(nativeCredential =>
{
Credential credential = new Credential();
credential.LoadInternal(nativeCredential);
return credential;
});
AddRange(existingCredentials);
// The individual credentials should not be free'd
credentialHandles.ForEach(handle => handle.SetHandleAsInvalid());
// Clean up memory to the Enumeration pointer
NativeMethods.CredFree(pCredentials);
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using System.Diagnostics.CodeAnalysis;
namespace GitHub.Authentication.CredentialManagement
{
[SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32",
Justification = "This is a uint as required by the unmanaged API")]
public enum CredentialType : uint
{
None = 0,
Generic = 1,
DomainPassword = 2,
DomainCertificate = 3,
DomainVisiblePassword = 4
}
}

Просмотреть файл

@ -0,0 +1,227 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace GitHub.Authentication.CredentialManagement
{
public static class NativeMethods
{
public const int CREDUI_MAX_USERNAME_LENGTH = 513;
public const int CREDUI_MAX_PASSWORD_LENGTH = 256;
public const int CREDUI_MAX_MESSAGE_LENGTH = 32767;
public const int CREDUI_MAX_CAPTION_LENGTH = 128;
[SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable"
, Justification = "This type needs to be this way for interop")]
[StructLayout(LayoutKind.Sequential)]
internal struct CREDENTIAL
{
public int Flags;
public int Type;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)]
public string Comment;
public long LastWritten;
public int CredentialBlobSize;
[SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources"
, Justification = "Need to validate that SafeHandle works properly with native interop")]
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)]
public string UserName;
}
[SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes",
Justification = "This type is soley for native interop")]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CREDUI_INFO
{
public int cbSize;
[SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible"
, Justification = "This is needed for native interop")]
public IntPtr hwndParent;
public string pszMessageText;
public string pszCaptionText;
[SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible"
, Justification = "This is needed for native interop")]
public IntPtr hbmBanner;
}
[Flags]
internal enum WINXP_CREDUI_FLAGS
{
INCORRECT_PASSWORD = 0x00001,
DO_NOT_PERSIST = 0x00002,
REQUEST_ADMINISTRATOR = 0x00004,
EXCLUDE_CERTIFICATES = 0x00008,
REQUIRE_CERTIFICATE = 0x00010,
SHOW_SAVE_CHECK_BOX = 0x00040,
ALWAYS_SHOW_UI = 0x00080,
REQUIRE_SMARTCARD = 0x00100,
PASSWORD_ONLY_OK = 0x00200,
VALIDATE_USERNAME = 0x00400,
COMPLETE_USERNAME = 0x00800,
PERSIST = 0x01000,
SERVER_CREDENTIAL = 0x04000,
EXPECT_CONFIRMATION = 0x20000,
GENERIC_CREDENTIALS = 0x40000,
USERNAME_TARGET_CREDENTIALS = 0x80000,
KEEP_USERNAME = 0x100000,
}
[Flags]
internal enum WINVISTA_CREDUI_FLAGS
{
/// <summary>
/// The caller is requesting that the credential provider return the user name and password in plain text.
/// This value cannot be combined with SECURE_PROMPT.
/// </summary>
CREDUIWIN_GENERIC = 0x1,
/// <summary>
/// The Save check box is displayed in the dialog box.
/// </summary>
CREDUIWIN_CHECKBOX = 0x2,
/// <summary>
/// Only credential providers that support the authentication package specified by the authPackage parameter should be enumerated.
/// This value cannot be combined with CREDUIWIN_IN_CRED_ONLY.
/// </summary>
CREDUIWIN_AUTHPACKAGE_ONLY = 0x10,
/// <summary>
/// Only the credentials specified by the InAuthBuffer parameter for the authentication package specified by the authPackage parameter should be enumerated.
/// If this flag is set, and the InAuthBuffer parameter is NULL, the function fails.
/// This value cannot be combined with CREDUIWIN_AUTHPACKAGE_ONLY.
/// </summary>
CREDUIWIN_IN_CRED_ONLY = 0x20,
/// <summary>
/// Credential providers should enumerate only administrators. This value is intended for User Account Control (UAC) purposes only. We recommend that external callers not set this flag.
/// </summary>
CREDUIWIN_ENUMERATE_ADMINS = 0x100,
/// <summary>
/// Only the incoming credentials for the authentication package specified by the authPackage parameter should be enumerated.
/// </summary>
CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200,
/// <summary>
/// The credential dialog box should be displayed on the secure desktop. This value cannot be combined with CREDUIWIN_GENERIC.
/// Windows Vista: This value is not supported until Windows Vista with SP1.
/// </summary>
CREDUIWIN_SECURE_PROMPT = 0x1000,
/// <summary>
/// The credential provider should align the credential BLOB pointed to by the refOutAuthBuffer parameter to a 32-bit boundary, even if the provider is running on a 64-bit system.
/// </summary>
CREDUIWIN_PACK_32_WOW = 0x10000000,
}
internal enum CredUIReturnCodes
{
NO_ERROR = 0,
ERROR_CANCELLED = 1223,
ERROR_NO_SUCH_LOGON_SESSION = 1312,
ERROR_NOT_FOUND = 1168,
ERROR_INVALID_ACCOUNT_NAME = 1315,
ERROR_INSUFFICIENT_BUFFER = 122,
ERROR_BAD_ARGUMENTS = 160,
ERROR_INVALID_PARAMETER = 87,
ERROR_INVALID_FLAGS = 1004,
}
internal enum CREDErrorCodes
{
NO_ERROR = 0,
ERROR_NOT_FOUND = 1168,
ERROR_NO_SUCH_LOGON_SESSION = 1312,
ERROR_INVALID_PARAMETER = 87,
ERROR_INVALID_FLAGS = 1004,
ERROR_BAD_USERNAME = 2202,
SCARD_E_NO_READERS_AVAILABLE = (int)(0x8010002E - 0x100000000),
SCARD_E_NO_SMARTCARD = (int)(0x8010000C - 0x100000000),
SCARD_W_REMOVED_CARD = (int)(0x80100069 - 0x100000000),
SCARD_W_WRONG_CHV = (int)(0x8010006B - 0x100000000)
}
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredFree([In] IntPtr cred);
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredDelete(StringBuilder target, CredentialType type, int flags);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredEnumerateW(string filter, int flag, out uint count, out IntPtr pCredentials);
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
internal static extern CredUIReturnCodes CredUIPromptForCredentials(ref CREDUI_INFO creditUR, string targetName, IntPtr reserved1, int iError, StringBuilder userName, int maxUserName, StringBuilder password, int maxPassword, [MarshalAs(UnmanagedType.Bool)] ref bool pfSave, int flags);
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
internal static extern CredUIReturnCodes CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere, int authError, ref uint authPackage, IntPtr InAuthBuffer, uint InAuthBufferSize, out IntPtr refOutAuthBuffer, out uint refOutAuthBufferSize, [MarshalAs(UnmanagedType.Bool)]ref bool fSave, int flags);
[DllImport("ole32.dll")]
internal static extern void CoTaskMemFree(IntPtr ptr);
[DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredPackAuthenticationBuffer(int dwFlags, StringBuilder pszUserName, StringBuilder pszPassword, IntPtr pPackedCredentials, ref int pcbPackedCredentials);
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CredUnPackAuthenticationBuffer(int dwFlags, IntPtr pAuthBuffer, uint cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword);
internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
{
// Set the handle.
internal CriticalCredentialHandle(IntPtr preexistingHandle)
{
SetHandle(preexistingHandle);
}
internal CREDENTIAL GetCredential()
{
if (!IsInvalid)
{
// Get the Credential from the mem location
return (CREDENTIAL)Marshal.PtrToStructure(handle, typeof(CREDENTIAL));
}
else
{
throw new InvalidOperationException("Invalid CriticalHandle!");
}
}
// Perform any specific actions to release the handle in the ReleaseHandle method.
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
override protected bool ReleaseHandle()
{
// If the handle was set, free it. Return success.
if (!IsInvalid)
{
// NOTE: We should also ZERO out the memory allocated to the handle, before free'ing it
// so there are no traces of the sensitive data left in memory.
CredFree(handle);
// Mark the handle as invalid for future users.
SetHandleAsInvalid();
return true;
}
// Return false.
return false;
}
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using System.Diagnostics.CodeAnalysis;
namespace GitHub.Authentication.CredentialManagement
{
[SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32"
, Justification = "This is this way for interop")]
[SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue"
, Justification = "I assume this is defined this way for Native interop")]
public enum PersistenceType : uint
{
Session = 1,
LocalComputer = 2,
Enterprise = 3
}
}

Просмотреть файл

@ -0,0 +1,47 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace GitHub.Authentication.CredentialManagement
{
[SuppressUnmanagedCodeSecurity]
internal static class SecureStringHelper
{
// Methods
internal static SecureString CreateSecureString(string plainString)
{
var str = new SecureString();
if (!string.IsNullOrEmpty(plainString))
{
foreach (char c in plainString)
{
str.AppendChar(c);
}
}
return str;
}
internal static string CreateString(SecureString secureString)
{
string str;
var zero = IntPtr.Zero;
if ((secureString == null) || (secureString.Length == 0))
{
return string.Empty;
}
try
{
zero = Marshal.SecureStringToBSTR(secureString);
str = Marshal.PtrToStringBSTR(zero);
}
finally
{
if (zero != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(zero);
}
}
return str;
}
}
}

Просмотреть файл

@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using Akavache;
using GitHub.Authentication.CredentialManagement;
using GitHub.Helpers;
namespace GitHub.Caches
{
public class CredentialCache : ISecureBlobCache, IObjectBlobCache
{
public IScheduler Scheduler { get; protected set; }
readonly AsyncSubject<Unit> shutdown = new AsyncSubject<Unit>();
public IObservable<Unit> Shutdown { get { return shutdown; } }
bool disposed = false;
public void Dispose()
{
GC.SuppressFinalize(this);
Scheduler = null;
shutdown.OnNext(Unit.Default);
shutdown.OnCompleted();
disposed = true;
}
public IObservable<Unit> Flush()
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
return Observable.Return(Unit.Default);
}
public IObservable<byte[]> Get(string key)
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<byte[]>("CredentialCache");
var keyHost = GetKeyHost(key);
using (var credential = new Credential())
{
credential.Target = keyHost;
credential.Type = CredentialType.Generic;
if (credential.Load())
return Observable.Return(Encoding.Unicode.GetBytes(credential.Password));
}
return ExceptionHelper.ObservableThrowKeyNotFoundException<byte[]>(key);
}
public IObservable<IEnumerable<string>> GetAllKeys()
{
throw new NotImplementedException();
}
public IObservable<DateTimeOffset?> GetCreatedAt(string key)
{
throw new NotImplementedException();
}
public IObservable<Unit> Insert(string key, byte[] data, DateTimeOffset? absoluteExpiration = default(DateTimeOffset?))
{
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(key);
}
public IObservable<Unit> Invalidate(string key)
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
var keyGit = GetKeyGit(key);
if (!DeleteKey(keyGit))
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyGit);
var keyHost = GetKeyHost(key);
if (!DeleteKey(keyHost))
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyHost);
return Observable.Return(Unit.Default);
}
public IObservable<Unit> InvalidateAll()
{
throw new NotImplementedException();
}
public IObservable<Unit> Vacuum()
{
throw new NotImplementedException();
}
// TODO: Use SecureString
public IObservable<Unit> InsertObject<T>(string key, T value, DateTimeOffset? absoluteExpiration = default(DateTimeOffset?))
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
Tuple<string, string> values = value as Tuple<string, string>;
if (values == null)
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(key);
var keyGit = GetKeyGit(key);
if (!SaveKey(keyGit, values.Item1, values.Item2))
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(keyGit);
var keyHost = GetKeyHost(key);
if (!SaveKey(keyHost, values.Item1, values.Item2))
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(keyGit);
return Observable.Return(Unit.Default);
}
public IObservable<T> GetObject<T>(string key)
{
if (typeof(T) == typeof(Tuple<string, string>))
return (IObservable<T>) GetTuple(key);
return ExceptionHelper.ObservableThrowInvalidOperationException<T>(key);
}
IObservable<Tuple<string, string>> GetTuple(string key)
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Tuple<string, string>>("CredentialCache");
var keyHost = GetKeyHost(key);
var ret = GetKey(keyHost);
if (ret != null)
return Observable.Return(ret);
return ExceptionHelper.ObservableThrowKeyNotFoundException<Tuple<string, string>>(keyHost);
}
public IObservable<IEnumerable<T>> GetAllObjects<T>()
{
throw new NotImplementedException();
}
public IObservable<DateTimeOffset?> GetObjectCreatedAt<T>(string key)
{
throw new NotImplementedException();
}
public IObservable<Unit> InvalidateObject<T>(string key)
{
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
var keyGit = GetKeyGit(key);
if (!DeleteKey(keyGit))
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyGit);
var keyHost = GetKeyHost(key);
if (!DeleteKey(keyHost))
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(key);
return Observable.Return(Unit.Default);
}
public IObservable<Unit> InvalidateAllObjects<T>()
{
throw new NotImplementedException();
}
static string FormatKey(string key)
{
if (key.StartsWith("login:", StringComparison.Ordinal))
key = key.Substring("login:".Length);
return key;
}
static string GetKeyGit(string key)
{
key = FormatKey(key);
// it appears this is how MS expects the host key
if (!key.StartsWith("git:", StringComparison.Ordinal))
key = "git:" + key;
if (key.EndsWith("/", StringComparison.Ordinal))
key = key.Substring(0, key.Length - 1);
return key;
}
static string GetKeyHost(string key)
{
key = FormatKey(key);
if (key.StartsWith("git:", StringComparison.Ordinal))
key = key.Substring("git:".Length);
if (!key.EndsWith("/", StringComparison.Ordinal))
key += '/';
return key;
}
static bool DeleteKey(string key)
{
using (var credential = new Credential())
{
credential.Target = key;
if (!credential.Load())
return false;
return credential.Delete();
}
}
static bool SaveKey(string key, string user, string pwd)
{
using (var credential = new Credential(user, pwd, key))
{
credential.Type = CredentialType.Generic;
credential.PersistenceType = PersistenceType.LocalComputer;
return credential.Save();
}
}
static Tuple<string, string> GetKey(string key)
{
using (var credential = new Credential())
{
credential.Target = key;
credential.Type = CredentialType.Generic;
if (credential.Load())
return new Tuple<string, string>(credential.Username, credential.Password);
return null;
}
}
}
}

Просмотреть файл

@ -25,7 +25,7 @@ namespace GitHub.Caches
//{
//}
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new InMemoryBlobCache())
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new CredentialCache())
{
}

Просмотреть файл

@ -84,6 +84,7 @@ namespace GitHub.Controllers
var twofa = uiProvider.GetService<ITwoFactorViewModel>();
twofa.WhenAny(x => x.IsShowing, x => x.Value)
.Where(x => x)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ =>
{
Fire(Trigger.Next);

Просмотреть файл

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<NullGuard/>
<NullGuard ExcludeRegex="^GitHub.Authentication.CredentialManagement.*$"/>
</Weavers>

Просмотреть файл

@ -49,6 +49,9 @@
<DelaySign>false</DelaySign>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.TeamFoundation.Git.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\lib\Microsoft.TeamFoundation.Git.Controls.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0" />
@ -105,6 +108,13 @@
<Link>Key.snk</Link>
</None>
<Compile Include="Extensions\FileExtensions.cs" />
<Compile Include="Authentication\CredentialManagement\Credential.cs" />
<Compile Include="Authentication\CredentialManagement\CredentialSet.cs" />
<Compile Include="Authentication\CredentialManagement\CredentialType.cs" />
<Compile Include="Authentication\CredentialManagement\NativeMethods.cs" />
<Compile Include="Authentication\CredentialManagement\PersistenceType.cs" />
<Compile Include="Authentication\CredentialManagement\SecureStringHelper.cs" />
<Compile Include="Caches\CredentialCache.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\..\script\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
@ -167,7 +177,9 @@
<Compile Include="ViewModels\TwoFactorDialogViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
<Content Include="FodyWeavers.xml">
<SubType>Designer</SubType>
</Content>
<Resource Include="Images\default_org_avatar.png" />
<Resource Include="Images\default_user_avatar.png" />
</ItemGroup>

Просмотреть файл

@ -177,7 +177,7 @@ namespace GitHub.Models
if (authorization == null || String.IsNullOrWhiteSpace(authorization.Token))
return Observable.Return(Unit.Default);
return LoginCache.SaveLogin(authorization.Token, "x-oauth-basic", Address)
return LoginCache.SaveLogin(usernameOrEmail, authorization.Token, Address)
.ObserveOn(RxApp.MainThreadScheduler);
});

Просмотреть файл

@ -1,56 +1,103 @@
using System;
using GitHub.Primitives;
using NullGuard;
using ReactiveUI;
using Octokit;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using System.Globalization;
namespace GitHub.Models
{
public class RepositoryModel : IRepositoryModel
public class RepositoryModel : ReactiveObject, IRepositoryModel
{
int? id;
string name;
string description;
bool isPrivate = true; // All repos are assumed to be private until proven otherwise.
readonly ObservableAsPropertyHelper<string> nameWithOwner;
string owner;
Uri hostUri;
UriString cloneUrl;
[AllowNull]
public string Description
{
get;
set;
[return: AllowNull]
get { return description; }
set { this.RaiseAndSetIfChanged(ref description, value); }
}
[AllowNull]
public HostAddress HostAddress
{
[return: AllowNull]
get;
set;
}
[AllowNull]
public Uri HostUri
{
get;
set;
[return: AllowNull]
get { return hostUri; }
set { this.RaiseAndSetIfChanged(ref hostUri, value); }
}
public int? Id
{
get;
set;
get { return id; }
set { this.RaiseAndSetIfChanged(ref id, value); }
}
public bool IsPrivate
{
get;
set;
get { return isPrivate; }
set { this.RaiseAndSetIfChanged(ref isPrivate, value); }
}
public string Name
{
get;
set;
get { return name; }
set { this.RaiseAndSetIfChanged(ref name, value); }
}
public string NameWithOwner
{
get;
set;
get { return nameWithOwner.Value; }
}
[AllowNull]
public string Owner
{
get;
set;
[return: AllowNull]
get { return owner; }
set { this.RaiseAndSetIfChanged(ref owner, value); }
}
[AllowNull]
public UriString CloneUrl
{
[return: AllowNull]
get { return cloneUrl; }
set { this.RaiseAndSetIfChanged(ref cloneUrl, value); }
}
public RepositoryModel()
{
nameWithOwner = this.WhenAny(x => x.Name, x => x.Owner, (name, owner) => new { Name = name.Value, Owner = owner.Value })
.Select(x => x.Name != null && x.Owner != null ? String.Format(CultureInfo.InvariantCulture, "{0}/{1}", x.Owner, x.Name) : null)
.ToProperty(this, x => x.NameWithOwner, null, Scheduler.Immediate);
}
public RepositoryModel(Repository repo)
{
Id = repo.Id;
Name = repo.Name;
Description = repo.Description;
Owner = repo.Owner.Login;
CloneUrl = repo.CloneUrl;
IsPrivate = repo.Private;
}
}
}

Просмотреть файл

@ -336,6 +336,8 @@ namespace GitHub.SampleData
private set;
}
public IRepositoryModel SelectedRepository { get; set; }
public string Title { get { return "Clone a GitHub Repository"; } }
}

Просмотреть файл

@ -1,21 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Windows.Input;
using GitHub.Exports;
using GitHub.Models;
using Microsoft.TeamFoundation.Git.Controls.Extensibility;
using NullGuard;
using Octokit;
using ReactiveUI;
namespace GitHub.ViewModels
{
[ExportViewModel(ViewType=UIViewType.Clone)]
public class RepositoryCloneViewModel : IRepositoryCloneViewModel
public class RepositoryCloneViewModel : ReactiveObject, IRepositoryCloneViewModel
{
public string Title { get { return "Clone a GitHub Repository"; } } // TODO: this needs to be contextual
IReactiveCommand<object> cloneCommand = ReactiveCommand.Create();
IReactiveCommand<object> cloneCommand;
public ICommand CloneCommand { get { return cloneCommand; } }
@ -25,8 +28,17 @@ namespace GitHub.ViewModels
private set;
}
IRepositoryModel _selectedRepository;
[AllowNull]
public IRepositoryModel SelectedRepository
{
[return: AllowNull]
get { return _selectedRepository; }
set { this.RaiseAndSetIfChanged(ref _selectedRepository, value); }
}
[ImportingConstructor]
public RepositoryCloneViewModel(IRepositoryHosts hosts)
public RepositoryCloneViewModel(IServiceProvider serviceProvider, IRepositoryHosts hosts)
{
// TODO: How do I know which host this dialog is associated with?
// For now, I'll assume GitHub Host.
@ -35,9 +47,28 @@ namespace GitHub.ViewModels
.Catch<User, KeyNotFoundException>(_ => Observable.Empty<User>())
.SelectMany(user => hosts.GitHubHost.ApiClient.GetUserRepositories(user.Id))
.SelectMany(repo => repo)
.Select(repo => new RepositoryModel { Owner = repo.Owner.Login, Name = repo.Name })
.Select(repo => new RepositoryModel(repo))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(Repositories.Add);
cloneCommand = ReactiveCommand.CreateAsyncObservable(_ =>
{
var repo = SelectedRepository;
var uri = repo.CloneUrl;
return Observable.Start<object>(() =>
{
var gitExt = serviceProvider.GetService(typeof(IGitRepositoriesExt)) as IGitRepositoriesExt;
Debug.Assert(gitExt != null, "Could not get an instance of IGitRepositoriesExt");
// TODO: use VS default dir for projects (https://github.com/github/VisualStudio/issues/96)
var tmp = System.IO.Path.GetTempFileName();
System.IO.File.Delete(tmp);
var tmpname = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(tmp), System.IO.Path.GetFileNameWithoutExtension(tmp));
System.IO.Directory.CreateDirectory(tmpname);
gitExt.Clone(uri.ToString(), tmpname, CloneOptions.RecurseSubmodule);
return null;
});
});
}
}
}

Просмотреть файл

@ -77,6 +77,8 @@
<None Include="..\..\script\Key.snk">
<Link>Key.snk</Link>
</None>
<Compile Include="Helpers\ExceptionHelper.cs" />
<Compile Include="Helpers\Guard.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\..\script\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>

Просмотреть файл

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GitHub.Helpers
{
public static class ExceptionHelper
{
public static IObservable<T> ObservableThrowKeyNotFoundException<T>(string key, Exception innerException = null)
{
return Observable.Throw<T>(
new KeyNotFoundException(String.Format(CultureInfo.InvariantCulture,
"The given key '{0}' was not present in the cache.", key), innerException));
}
public static IObservable<T> ObservableThrowObjectDisposedException<T>(string obj, Exception innerException = null)
{
return Observable.Throw<T>(
new ObjectDisposedException(String.Format(CultureInfo.InvariantCulture,
"The cache '{0}' was disposed.", obj), innerException));
}
public static IObservable<T> ObservableThrowInvalidOperationException<T>(string obj, Exception innerException = null)
{
return Observable.Throw<T>(
new InvalidOperationException(obj, innerException));
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
using System.Diagnostics;
using System.Globalization;
using Splat;
namespace GitHub
{
public static class Guard
{
/// <summary>
/// Validates that the string is not empty.
/// </summary>
/// <param name="value"></param>
public static void ArgumentNotEmptyString(string value, string name)
{
// We already know the value is not null because of NullGuard.Fody.
if (!string.IsNullOrWhiteSpace(value)) return;
string message = string.Format(CultureInfo.InvariantCulture, "The value for '{0}' must not be empty", name);
#if DEBUG
if (!ModeDetector.InUnitTestRunner())
{
Debug.Fail(message);
}
#endif
throw new ArgumentException("String cannot be empty", name);
}
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class ValidatedNotNullAttribute : Attribute
{
}
}
}

Просмотреть файл

@ -13,5 +13,6 @@ namespace GitHub.Models
string Description { get; }
Uri HostUri { get; }
bool IsPrivate { get; }
UriString CloneUrl { get; }
}
}

Просмотреть файл

@ -44,14 +44,16 @@ namespace GitHub.Primitives
{
WebUri = new Uri(enterpriseUri, new Uri("/", UriKind.Relative));
ApiUri = new Uri(enterpriseUri, new Uri("/api/v3/", UriKind.Relative));
CredentialCacheKeyHost = ApiUri.Host;
//CredentialCacheKeyHost = ApiUri.Host;
CredentialCacheKeyHost = WebUri.ToString();
}
private HostAddress()
{
WebUri = new Uri("https://github.com");
ApiUri = new Uri("https://api.github.com");
CredentialCacheKeyHost = "github.com";
//CredentialCacheKeyHost = "github.com";
CredentialCacheKeyHost = WebUri.ToString();
}
/// <summary>

Просмотреть файл

@ -6,6 +6,7 @@ namespace GitHub.Services
public interface IUIProvider
{
ExportProvider ExportProvider { get; }
IServiceProvider GitServiceProvider { get; set; }
object GetService(Type t);
T GetService<T>();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]

Просмотреть файл

@ -18,5 +18,7 @@ namespace GitHub.ViewModels
/// The list of repositories the current user may clone from the specified host.
/// </summary>
ICollection<IRepositoryModel> Repositories { get; }
IRepositoryModel SelectedRepository { get; set; }
}
}

Просмотреть файл

@ -76,6 +76,9 @@
<Reference Include="Microsoft.TeamFoundation.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Microsoft.TeamFoundation.Controls.dll</HintPath>
</Reference>
<Reference Include="Microsoft.TeamFoundation.Git.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\lib\Microsoft.TeamFoundation.Git.Controls.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />

Просмотреть файл

@ -19,6 +19,16 @@ namespace GitHub.VisualStudio
[AllowNull]
public ExportProvider ExportProvider { get; private set; }
IServiceProvider gitServiceProvider;
[AllowNull]
public IServiceProvider GitServiceProvider {
get { return gitServiceProvider; }
set {
if (gitServiceProvider == null)
gitServiceProvider = value;
}
}
readonly IServiceProvider serviceProvider;
[ImportingConstructor]
@ -43,6 +53,13 @@ namespace GitHub.VisualStudio
if (instance != null)
return instance;
if (gitServiceProvider != null)
{
instance = gitServiceProvider.GetService(serviceType);
if (instance != null)
return instance;
}
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Could not locate any instances of contract {0}.", contract));
}

Просмотреть файл

@ -7,6 +7,7 @@ using GitHub.VisualStudio.UI;
using GitHub.VisualStudio.UI.Views;
using Microsoft.TeamFoundation.Controls;
using Microsoft.VisualStudio.Shell;
using Microsoft.TeamFoundation.Git.Controls.Extensibility;
namespace GitHub.VisualStudio.TeamExplorerConnect
{
@ -15,6 +16,8 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
{
public const string PlaceholderGitHubSectionId = "519B47D3-F2A9-4E19-8491-8C9FA25ABE97";
IServiceProvider gitServiceProvider;
protected GitHubConnectContent View
{
get { return this.SectionContent as GitHubConnectContent; }
@ -34,12 +37,20 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
View.ViewModel = this;
}
public override void Initialize(object sender, SectionInitializeEventArgs e)
{
base.Initialize(sender, e);
gitServiceProvider = e.ServiceProvider;
}
public void DoCreate()
{
// this is done here and not via the constructor so nothing gets loaded
// until we get here
var ui = ServiceProvider.GetExportedValue<IUIProvider>();
ui.GitServiceProvider = gitServiceProvider;
var factory = ui.GetService<ExportFactoryProvider>();
var d = factory.UIControllerFactory.CreateExport();
var creation = d.Value.SelectFlow(UIControllerFlow.Create);
@ -55,7 +66,7 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
// this is done here and not via the constructor so nothing gets loaded
// until we get here
var ui = ServiceProvider.GetExportedValue<IUIProvider>();
ui.GitServiceProvider = gitServiceProvider;
var factory = ui.GetService<ExportFactoryProvider>();
var d = factory.UIControllerFactory.CreateExport();
var creation = d.Value.SelectFlow(UIControllerFlow.Clone);

Просмотреть файл

@ -32,6 +32,7 @@ namespace GitHub.VisualStudio.UI.Views.Controls
this.WhenActivated(d =>
{
d(this.OneWayBind(ViewModel, vm => vm.Repositories, v => v.repositoryList.ItemsSource, CreateRepositoryListCollectionView));
d(this.Bind(ViewModel, vm => vm.SelectedRepository, v => v.repositoryList.SelectedItem));
d(this.BindCommand(ViewModel, vm => vm.CloneCommand, v => v.cloneButton));
});
}

@ -1 +1 @@
Subproject commit 5d32f2581c1b0ce38c969c9ea63cf60c4562dee8
Subproject commit b83b3ff68796901378bdc215bed6c7bbe3d996f0