303 lines
14 KiB
C#
303 lines
14 KiB
C#
|
using System;
|
|||
|
using System.Linq;
|
|||
|
using System.ServiceModel;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using Alkami.Ops.SecretServer.Enum;
|
|||
|
using Alkami.Ops.SecretServer.Messages;
|
|||
|
using Alkami.Ops.SecretServer.Model;
|
|||
|
using Alkami.Ops.SecretServer.SSWebService;
|
|||
|
|
|||
|
namespace Alkami.Ops.SecretServer
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// A simple client for secret server
|
|||
|
/// </summary>
|
|||
|
public sealed class Client : IDisposable
|
|||
|
{
|
|||
|
private string _authenticationToken;
|
|||
|
|
|||
|
private readonly SSWebServiceSoapClient _client;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets all secrets from a folder based on folder name
|
|||
|
/// </summary>
|
|||
|
/// <param name="folderName"></param>
|
|||
|
/// <returns></returns>
|
|||
|
public async Task<SecretSearchResult> GetFolderSecretsAsync(string folderName)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting to find all secrets in folder: {folderName}");
|
|||
|
var folderSearchResult = await FindFolderAsync(folderName);
|
|||
|
|
|||
|
if (folderSearchResult.Status == ResultStatus.Success)
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting to get all secrets from folder with name: {folderName} and folder ID: {folderSearchResult.Folders.First().Id}");
|
|||
|
return await GetAllSecretsFromFolderAsync(folderSearchResult.Folders.First());
|
|||
|
}
|
|||
|
|
|||
|
Console.WriteLine($"Attempt to find folder {folderName} resulted in errors: {string.Join(",", folderSearchResult.Errors)}");
|
|||
|
return new SecretSearchResult
|
|||
|
{
|
|||
|
Errors = folderSearchResult.Errors,
|
|||
|
Message = folderSearchResult.Message
|
|||
|
};
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Console.WriteLine($"An error occurred while attempting to get all secrets from folder with name: {folderName}. Exception: {e}");
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Authenticates using the supplied username, password, and domain
|
|||
|
/// </summary>
|
|||
|
/// <param name="userName"></param>
|
|||
|
/// <param name="password"></param>
|
|||
|
/// <param name="domain"></param>
|
|||
|
/// <returns></returns>
|
|||
|
public async Task<AuthenticationResult> AuthenticateAsync(string userName, string password, string domain)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting authentication with username: {userName} and domain {domain}");
|
|||
|
var result = await _client.AuthenticateAsync(userName, password, string.Empty, domain);
|
|||
|
|
|||
|
Console.WriteLine(result.Errors != null && result.Errors.Any()
|
|||
|
? $"Encountered errors during authentication: {string.Join(",", result.Errors)}"
|
|||
|
: "Authentication successful");
|
|||
|
|
|||
|
return new AuthenticationResult
|
|||
|
{
|
|||
|
Status = (result.Errors != null && result.Errors.Any() ? ResultStatus.Failure : ResultStatus.Success),
|
|||
|
Message = (result.Errors != null && result.Errors.Any() ? "Authentication Failed" : "Authentication Successful"),
|
|||
|
Errors = result.Errors?.Select(e => new ResultError(null, e)).ToList(),
|
|||
|
Token = _authenticationToken = result.Token
|
|||
|
};
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Console.WriteLine($"An error occurred while attempting to authentice with username: {userName} and domain {domain}. Exception: {e}");
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns a collection of secrets from a folder as a <see cref="SecretSearchResult"/>
|
|||
|
/// </summary>
|
|||
|
/// <param name="folder"></param>
|
|||
|
/// <returns></returns>
|
|||
|
private async Task<SecretSearchResult> GetAllSecretsFromFolderAsync(Folder folder)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting to pull all secrets from folder {folder.Name}");
|
|||
|
var folderSearchResponse = await _client.SearchSecretsByFolderAsync(_authenticationToken, "*", folder.Id, false, false, true);
|
|||
|
var results = new SecretSearchResult();
|
|||
|
|
|||
|
if (folderSearchResponse.SearchSecretsByFolderResult.Errors != null && folderSearchResponse.SearchSecretsByFolderResult.Errors.Any())
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempt to pull secrets from folder {folder.Name} resulted in errors: {string.Join(",", folderSearchResponse.SearchSecretsByFolderResult.Errors)}");
|
|||
|
results.Errors = folderSearchResponse.SearchSecretsByFolderResult.Errors.Select(e => new ResultError(null, e)).ToList();
|
|||
|
results.Status = ResultStatus.Failure;
|
|||
|
}
|
|||
|
|
|||
|
foreach (var secretSummary in folderSearchResponse.SearchSecretsByFolderResult.SecretSummaries)
|
|||
|
{
|
|||
|
Console.WriteLine($"Getting secret details for secret ID: {secretSummary.SecretId} with name: {secretSummary.SecretName}");
|
|||
|
var secretResponse = await _client.GetSecretAsync(_authenticationToken, secretSummary.SecretId, false, null);
|
|||
|
|
|||
|
if (secretResponse.GetSecretResult.Errors.Any())
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempt to pull secrets from folder {folder.Name} resulted in errors: {string.Join(",", secretResponse.GetSecretResult.Errors)}");
|
|||
|
results.Errors.Add(new ResultError(secretSummary.SecretId,
|
|||
|
$"Error {secretResponse.GetSecretResult.Errors[0]} occurred while getting secret {secretSummary.SecretId}"));
|
|||
|
results.Status = ResultStatus.PartialFailure;
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
switch (secretSummary.SecretTypeName.ToUpperInvariant())
|
|||
|
{
|
|||
|
case "SSL CERTIFICATE":
|
|||
|
case "CERTIFICATE":
|
|||
|
{
|
|||
|
Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type Certificate");
|
|||
|
var certificate = new Certificate(secretResponse.GetSecretResult.Secret);
|
|||
|
|
|||
|
if (!certificate.FileId.HasValue)
|
|||
|
{
|
|||
|
Console.WriteLine($"Certificate with secret ID {secretSummary.SecretId} does not have an attachment");
|
|||
|
results.Errors.Add(new ResultError(secretSummary.SecretId,
|
|||
|
$"Unable to find a certificate attachemnt for secret {certificate.Id}"));
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
// Download the PFX Files
|
|||
|
Console.WriteLine($"Attempting to download attachment for Certificate with secret ID {secretSummary.SecretId}");
|
|||
|
var certificateAttachment = await DownloadFileAsync(certificate.Id, certificate.FileId.Value);
|
|||
|
|
|||
|
certificate.FileAttachment = certificateAttachment.FileAttachment;
|
|||
|
|
|||
|
certificate.FileName = certificateAttachment.FileName;
|
|||
|
results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, certificate);
|
|||
|
break;
|
|||
|
}
|
|||
|
case "WINDOWS ACCOUNT":
|
|||
|
{
|
|||
|
Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type User");
|
|||
|
results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, new Model.User(secretResponse.GetSecretResult.Secret));
|
|||
|
break;
|
|||
|
}
|
|||
|
case "SQL SERVER ACCOUNT":
|
|||
|
{
|
|||
|
Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type ConnectionString");
|
|||
|
results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, new ConnectionString(secretResponse.GetSecretResult.Secret));
|
|||
|
break;
|
|||
|
}
|
|||
|
default:
|
|||
|
{
|
|||
|
Console.WriteLine($"Secret with ID {secretSummary.SecretId} is an unmapped type");
|
|||
|
results.Errors.Add(new ResultError(secretSummary.SecretId,
|
|||
|
$"Unknown secret type {secretSummary.SecretTypeName} found for secret ID {secretSummary.SecretId}"));
|
|||
|
results.Status = ResultStatus.PartialFailure;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!results.Secrets.Any())
|
|||
|
{
|
|||
|
// All secret searches failed
|
|||
|
results.Status = ResultStatus.Failure;
|
|||
|
}
|
|||
|
else if (results.Status == ResultStatus.Unknown)
|
|||
|
{
|
|||
|
// All secret searches passed, otherwise this would be PartialFailure
|
|||
|
results.Status = ResultStatus.Success;
|
|||
|
}
|
|||
|
|
|||
|
return results;
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Console.WriteLine($"An error occurred while attempting to pull all secrets from folder {folder.Name}. Exception: {e}");
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Downloads a file based on the parent Secret and File ItemId
|
|||
|
/// </summary>
|
|||
|
/// <param name="secretId"></param>
|
|||
|
/// <param name="fileItemId"></param>
|
|||
|
/// <returns></returns>
|
|||
|
private async Task<Messages.FileDownloadResult> DownloadFileAsync(int secretId, int fileItemId)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting to download file attachment for secret with ID: {secretId} and fileItemID: {fileItemId}");
|
|||
|
var downloadResponse = await _client.DownloadFileAttachmentByItemIdAsync(_authenticationToken, secretId, fileItemId);
|
|||
|
var result = new Messages.FileDownloadResult();
|
|||
|
|
|||
|
if (downloadResponse.Errors.Any() || downloadResponse.FileAttachment.Length == 0)
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempt to download attachment from secret with ID {secretId} resulted in errors: {string.Join(",", downloadResponse.Errors)}");
|
|||
|
result.Status = ResultStatus.Failure;
|
|||
|
result.Errors = downloadResponse.Errors.Select(e => new ResultError(secretId, e)).ToList();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
result.Status = ResultStatus.Success;
|
|||
|
result.FileAttachment = downloadResponse.FileAttachment;
|
|||
|
result.FileName = downloadResponse.FileName;
|
|||
|
}
|
|||
|
|
|||
|
return result;
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Console.WriteLine($"An error occurred while attempting to download file attachment for secret with ID: {secretId} and fileItemID: {fileItemId}. Exception: {e}");
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finds a Secret Folder based on FolderName
|
|||
|
/// </summary>
|
|||
|
/// <param name="folderName"></param>
|
|||
|
/// <returns></returns>
|
|||
|
private async Task<FolderSearchResult> FindFolderAsync(string folderName)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempting to find folder with name: {folderName}");
|
|||
|
var folderResponse = await _client.SearchFoldersAsync(_authenticationToken, folderName);
|
|||
|
|
|||
|
if (!folderResponse.Errors.Any() && folderResponse.Folders.Count() == 1)
|
|||
|
{
|
|||
|
return new FolderSearchResult
|
|||
|
{
|
|||
|
Status = ResultStatus.Success,
|
|||
|
Folders = folderResponse.Folders
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
if (folderResponse.Errors.Any())
|
|||
|
{
|
|||
|
Console.WriteLine($"Attempt to find folder with name {folderName} resulted in errors: {string.Join(",", folderResponse.Errors)}");
|
|||
|
}
|
|||
|
else if (folderResponse.Folders.Count() > 1)
|
|||
|
{
|
|||
|
Console.WriteLine("More than one folder was returned -- narrow your search and/or check the secret folder configuration.");
|
|||
|
}
|
|||
|
|
|||
|
return new FolderSearchResult
|
|||
|
{
|
|||
|
Errors = folderResponse.Errors.Select(e => new ResultError(null, e)).ToList(),
|
|||
|
Status = ResultStatus.Failure,
|
|||
|
Message = $"Folder Search for {folderName} Failed"
|
|||
|
};
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Console.WriteLine($"An error occurred while attempting to find folder with name: {folderName}. Exception: {e}");
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Instantiates a new instance of the Client class
|
|||
|
/// </summary>
|
|||
|
public Client()
|
|||
|
{
|
|||
|
const string endpointAddress = "https://secret.corp.alkamitech.com/webservices/SSWebService.asmx";
|
|||
|
var binding = new BasicHttpBinding
|
|||
|
{
|
|||
|
Name = "SSWebServiceSoap",
|
|||
|
Security =
|
|||
|
{
|
|||
|
Mode = BasicHttpSecurityMode.Transport
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
var endpoint = new EndpointAddress(endpointAddress);
|
|||
|
_client = new SSWebServiceSoapClient(binding, endpoint);
|
|||
|
}
|
|||
|
|
|||
|
#region Implement IDisposable
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Implementation of IDisposable.
|
|||
|
/// </summary>
|
|||
|
/// <remarks>
|
|||
|
/// Calls Dispose on the <see cref="SSWebServiceSoapClient"/>
|
|||
|
/// </remarks>
|
|||
|
public void Dispose()
|
|||
|
{
|
|||
|
((IDisposable)_client)?.Dispose();
|
|||
|
}
|
|||
|
|
|||
|
#endregion Implement IDisposable
|
|||
|
}
|
|||
|
}
|