701 lines
29 KiB
C#
701 lines
29 KiB
C#
using Alkami.Ops.Certificates.SecretServer.Models;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Alkami.Ops.Certificates.SecretServer
|
|
{
|
|
internal class SecretServerClient : IDisposable
|
|
{
|
|
public readonly string Site;
|
|
private readonly string _username;
|
|
private readonly string _password;
|
|
private readonly string _apiEndpoint;
|
|
private string _accessToken;
|
|
|
|
private HttpClient _httpClient;
|
|
|
|
public SecretServerClient(string site, string username, string password)
|
|
{
|
|
this.Site = site;
|
|
this._username = username;
|
|
this._password = password;
|
|
|
|
_apiEndpoint = $"{Site}/api/v1";
|
|
|
|
// Create httpclient.
|
|
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
|
|
_httpClient = new HttpClient();
|
|
|
|
// Obtain auth token.
|
|
Authenticate().Wait();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authenticates and grabs an oauth token, which is attached to common headers for future usage of the secret server client.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private async Task Authenticate()
|
|
{
|
|
var data = new
|
|
{
|
|
username = _username,
|
|
password = _password,
|
|
grant_type = "password"
|
|
};
|
|
var test = $"username={_username}&password={_password}&grant_type=password";
|
|
|
|
var body = JsonConvert.SerializeObject(data);
|
|
var content = new StringContent(test);
|
|
|
|
var tokenRoute = $"{Site}/oauth2/token";
|
|
var response = await _httpClient.PostAsync(tokenRoute, content);
|
|
var jsonResponse = await response.Content.ReadAsStringAsync();
|
|
|
|
JObject parsedResponse = JObject.Parse(jsonResponse);
|
|
_accessToken = parsedResponse["access_token"].ToString();
|
|
|
|
// Add auth token to the default headers of the http client.
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_accessToken}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a single folder with the name.
|
|
/// Throws an exception if there is more than one server with a particular folder name.
|
|
/// </summary>
|
|
/// <param name="folderName">The name of the folder to search for.</param>
|
|
/// <param name="parentID">The parent folder ID, to look for names within a particular folder.</param>
|
|
/// <returns>The Folder</returns>
|
|
public Folder GetFolderByName(string folderName, int parentID = -1)
|
|
{
|
|
var folders = GetFoldersByName(folderName, parentID);
|
|
|
|
// Limit to exact name. Searching for POD1 will also return POD10
|
|
folders = folders.Where(folder => string.Equals(folder.FolderName, folderName, StringComparison.OrdinalIgnoreCase)).ToArray();
|
|
|
|
if (folders == null || folders.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
else if (folders.Length > 1)
|
|
{
|
|
throw new Exception($"Found more than one folder with name {folderName}. Investigate.");
|
|
}
|
|
|
|
return folders.First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns folders with a given name.
|
|
/// Only returns the first page of results (10 entries), because their documentation doesn't describe how pagination works!
|
|
/// </summary>
|
|
/// <param name="folderName">The name of the folder to search for</param>
|
|
/// <returns>The Folder</returns>
|
|
public Folder[] GetFoldersByName(string folderName, int parentID = -1)
|
|
{
|
|
var parameters = $"?filter.includeRestricted=true&filter.searchText={folderName}";
|
|
if (parentID >= 0)
|
|
{
|
|
parameters += $"&filter.parentFolderId={parentID}";
|
|
}
|
|
|
|
var url = $"{_apiEndpoint}/folders{parameters}";
|
|
var folders = GetPageEnumerable<Folder>(url);
|
|
return folders.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the folders underneath a parent folder.
|
|
/// </summary>
|
|
/// <param name="parentID">The folder to pull folders from.</param>
|
|
/// <returns></returns>
|
|
public Folder[] GetFoldersByParentFolder(int parentID)
|
|
{
|
|
if (parentID <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var parameters = $"?filter.parentFolderId={parentID}";
|
|
var url = $"{_apiEndpoint}/folders/{parameters}";
|
|
var folders = GetPageEnumerable<Folder>(url);
|
|
return folders.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a secret server folder given a parent folder.
|
|
/// </summary>
|
|
/// <param name="parentFolder">The parent folder of the folder to create.</param>
|
|
/// <param name="folderName">The name of the folder to create.</param>
|
|
/// <param name="inheritPermissions">True if the folder should inherit the permissions of the parent folder.</param>
|
|
/// <param name="inheritSecretPolicy">True if the folder should inherit the secret policies of the parent folder.</param>
|
|
/// <returns>The Folder</returns>
|
|
public async Task<Folder> CreateFolderAsync(Folder parentFolder, string folderName, bool inheritPermissions = true, bool inheritSecretPolicy = true)
|
|
{
|
|
if (parentFolder == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return await CreateFolderAsync(parentFolder.ID, folderName, inheritPermissions, inheritSecretPolicy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a secret server folder given a parent folder.
|
|
/// </summary>
|
|
/// <param name="parentFolderId">The parent folder ID of the folder to create.</param>
|
|
/// <param name="folderName">The name of the folder to create.</param>
|
|
/// <param name="inheritPermissions">True if the folder should inherit the permissions of the parent folder.</param>
|
|
/// <param name="inheritSecretPolicy">True if the folder should inherit the secret policies of the parent folder.</param>
|
|
/// <returns>The Folder</returns>
|
|
public async Task<Folder> CreateFolderAsync(int parentFolderId, string folderName, bool inheritPermissions = true, bool inheritSecretPolicy = true)
|
|
{
|
|
var response = await _httpClient.GetAsync($"{_apiEndpoint}/folders/stub");
|
|
var jsonResponse = await response.Content.ReadAsStringAsync();
|
|
|
|
JObject parsedResponse = JObject.Parse(jsonResponse);
|
|
|
|
parsedResponse["folderName"] = folderName;
|
|
parsedResponse["folderTypeId"] = 1;
|
|
parsedResponse["inheritPermissions"] = inheritPermissions;
|
|
parsedResponse["inheritSecretPolicy"] = inheritSecretPolicy;
|
|
parsedResponse["parentFolderId"] = parentFolderId;
|
|
|
|
var createFolderArgsJson = parsedResponse.ToString();
|
|
var createFolderContentBytes = Encoding.UTF8.GetBytes(createFolderArgsJson);
|
|
var byteContent = new ByteArrayContent(createFolderContentBytes);
|
|
byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
|
var createResponse = await _httpClient.PostAsync($"{_apiEndpoint}/folders", byteContent);
|
|
var createResponseJson = await response.Content.ReadAsStringAsync();
|
|
|
|
return GetFolderByName(folderName, parentFolderId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all secrets inside the given folder. Be careful with this, really!
|
|
/// </summary>
|
|
/// <param name="folder">The folder to remove secrets in.</param>
|
|
/// <returns></returns>
|
|
public async Task DeleteSecretsInFolder(Folder folder)
|
|
{
|
|
var secrets = GetSecretsByFolder(folder, true);
|
|
foreach (var secret in secrets)
|
|
{
|
|
await DeleteSecret(secret.ID);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a folder from the secret server with a given folder path. Returns null if the folder does not exist.
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <returns></returns>
|
|
public Folder GetFolder(string folderPath)
|
|
{
|
|
// Swap slashes just in case and parse apart the folder path.
|
|
folderPath = folderPath.Replace("\\", "/");
|
|
var folderSplit = folderPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// Find the parent folder.
|
|
string parentFolderName = folderSplit[0];
|
|
Folder parentFolder = GetFolderByName(parentFolderName);
|
|
if (parentFolder == null)
|
|
{
|
|
throw new Exception($"Could not find parent folder named {parentFolderName}");
|
|
}
|
|
|
|
// Find, or create, the other folders up through the path.
|
|
foreach (var subfolder in folderSplit.Skip(1))
|
|
{
|
|
var folder = GetFolderByName(subfolder, parentFolder.ID);
|
|
|
|
if (folder == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
parentFolder = folder;
|
|
}
|
|
|
|
return parentFolder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a folder from the secret server with a given folder path, or creates the folders if they do not exist.
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <returns></returns>
|
|
public async Task<Folder> GetOrAddFolderAsync(string folderPath)
|
|
{
|
|
// Swap slashes just in case and parse apart the folder path.
|
|
folderPath = folderPath.Replace("\\", "/");
|
|
var folderSplit = folderPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// Find the parent folder.
|
|
string parentFolderName = folderSplit[0];
|
|
Folder parentFolder = GetFolderByName(parentFolderName);
|
|
if (parentFolder == null)
|
|
{
|
|
throw new Exception($"Could not find parent folder named {parentFolderName}");
|
|
}
|
|
|
|
// Find, or create, the other folders up through the path.
|
|
foreach (var subfolder in folderSplit.Skip(1))
|
|
{
|
|
var folder = GetFolderByName(subfolder, parentFolder.ID);
|
|
|
|
if (folder == null)
|
|
{
|
|
folder = await CreateFolderAsync(parentFolder, subfolder);
|
|
}
|
|
|
|
parentFolder = folder;
|
|
}
|
|
|
|
return parentFolder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a specific secret template type by name.
|
|
/// Throws an exception if the template name matches more than one template type.
|
|
/// </summary>
|
|
/// <param name="templateName">Name of the template to search for.</param>
|
|
/// <returns>The template with the given name.</returns>
|
|
public SecretTemplate GetSecretTemplateByName(string templateName)
|
|
{
|
|
var templates = GetSecretTemplatesByName(templateName);
|
|
|
|
// Limit to exact name. The secret API search is open-ended wildcard style.
|
|
templates = templates.Where(template => string.Equals(template.Name, template.Name, StringComparison.OrdinalIgnoreCase)).ToArray();
|
|
|
|
if (templates == null || templates.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
else if (templates.Length > 1)
|
|
{
|
|
throw new Exception($"There was more than one template type with the name {templateName}");
|
|
}
|
|
else
|
|
{
|
|
return templates.First();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns secret template types given a name.
|
|
/// </summary>
|
|
/// <param name="templateName">Name of the template.</param>
|
|
/// <returns></returns>
|
|
public SecretTemplate[] GetSecretTemplatesByName(string templateName)
|
|
{
|
|
var searchString = $"?filter.searchText={templateName}";
|
|
var url = $"{_apiEndpoint}/secret-templates{searchString}";
|
|
var secretTemplates = GetPageEnumerable<SecretTemplate>(url);
|
|
return secretTemplates.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of all secrets from a given folder.
|
|
/// Secrets returned only include top-level information, and does not include fields or extended properties.
|
|
/// Use GetSecretByID() to grab all of the information for a particular secret.
|
|
/// </summary>
|
|
/// <param name="folder">Folder to pull secrets from</param>
|
|
/// <param name="includeSubfolders">True if you want the result to include secrets from subfolders.</param>
|
|
/// <returns></returns>
|
|
public Secret[] GetSecretsByFolder(Folder folder, bool includeSubfolders = false)
|
|
{
|
|
var filter = $"?filter.folderId={folder.ID}";
|
|
if (includeSubfolders)
|
|
{
|
|
filter += "&filter.includeSubFolders=True";
|
|
}
|
|
var url = $"{_apiEndpoint}/secrets/{filter}";
|
|
|
|
var secrets = GetPageEnumerable<Secret>(url);
|
|
return secrets.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a Secret Server style paging enumerable that will continue to load pages as the IEnumerable is enumerated.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of the object to deserialize from the request.</typeparam>
|
|
/// <param name="url">The query URL</param>
|
|
/// <returns></returns>
|
|
private IEnumerable<T> GetPageEnumerable<T>(string url)
|
|
{
|
|
bool hasParameters = url.IndexOf('?') >= 0;
|
|
int pageCounter = 0;
|
|
int numPages = 0;
|
|
int entriesPerPage = 10;
|
|
do
|
|
{
|
|
// Build the skip/take params depending on the page.
|
|
int skip = entriesPerPage * pageCounter;
|
|
string paramString = hasParameters ? "&" : "?";
|
|
paramString += $"Skip={skip}&Take={entriesPerPage}";
|
|
string endpoint = url + paramString;
|
|
|
|
// Unfortunately async enumerables won't exist until C# 8.
|
|
// The sync enumerable is preferable to duplicating this code everywhere.
|
|
var response = _httpClient.GetAsync(endpoint, HttpCompletionOption.ResponseContentRead).GetAwaiter().GetResult();
|
|
var jsonResponse = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
|
|
JObject parsedResponse = JObject.Parse(jsonResponse);
|
|
int total = int.Parse(parsedResponse["total"].ToString());
|
|
if (total <= 0)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
// Figure out how many pages, and how many entries per page there are.
|
|
entriesPerPage = int.Parse(parsedResponse["take"].ToString());
|
|
numPages = int.Parse(parsedResponse["pageCount"].ToString());
|
|
int currentPage = int.Parse(parsedResponse["currentPage"].ToString());
|
|
|
|
var records = parsedResponse["records"];
|
|
var count = records.Count();
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
yield return records[i].ToObject<T>();
|
|
}
|
|
|
|
++pageCounter;
|
|
} while (pageCounter < numPages);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a secret (with full data) with the given ID.
|
|
/// </summary>
|
|
/// <param name="ID">ID of the secret.</param>
|
|
/// <returns></returns>
|
|
public async Task<Secret> GetSecretByID(int ID)
|
|
{
|
|
var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{ID}");
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
var result = JsonConvert.DeserializeObject<Secret>(responseContent);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up a single secret by name.
|
|
/// Throws an exception if there is more than one secret that matches the name.
|
|
/// </summary>
|
|
/// <param name="name">Name of the secret</param>
|
|
/// <param name="folderID">Optional parent folder ID</param>
|
|
/// <returns></returns>
|
|
public SecretSearch FindSecretByName(string name, int folderID = -1)
|
|
{
|
|
var secrets = FindSecretsByName(name, folderID)
|
|
?.Where(secret => string.Equals(secret?.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase));
|
|
int count = secrets.Count();
|
|
if (secrets == null || count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
else if (count > 1)
|
|
{
|
|
throw new Exception($"Found more than one secret with name {name}");
|
|
}
|
|
|
|
return secrets.First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up secrets by name.
|
|
/// </summary>
|
|
/// <param name="name">Name of the secret</param>
|
|
/// <param name="folderID">Optional parent folder ID</param>
|
|
/// <returns></returns>
|
|
public SecretSearch[] FindSecretsByName(string name, int folderID = -1)
|
|
{
|
|
var filters = $"?filter.includeRestricted=true&filter.searchtext={name}";
|
|
if (folderID > 0)
|
|
{
|
|
filters += $"&filter.folderId={folderID}";
|
|
}
|
|
var url = $"{_apiEndpoint}/secrets/lookup{filters}";
|
|
var secrets = GetPageEnumerable<SecretSearch>(url);
|
|
return secrets.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a shimmed out default-value secret modeled after the provided templatID.
|
|
/// </summary>
|
|
/// <param name="templateId">Template ID to model the secret after</param>
|
|
/// <param name="folderId">Folder to place the secret in.</param>
|
|
/// <returns></returns>
|
|
public async Task<Secret> CreateSecretStubByTemplateIDAsync(int templateId, string secretName, int folderId)
|
|
{
|
|
var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/stub?filter.secrettemplateid={templateId}&filter.folderId={folderId}");
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
var result = JsonConvert.DeserializeObject<Secret>(responseContent);
|
|
result.Name = secretName;
|
|
result.SiteID = 1;
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a secret, given a secret object.
|
|
/// It is highly suggested that you create secrets with CreateSecretStubByTemplateID in order to construct a valid Secret object based on a template type.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to create.</param>
|
|
/// <returns></returns>
|
|
public async Task<Secret> CreateSecret(Secret secret)
|
|
{
|
|
// Do some basic validation to make sure this isn't going to do something undefined.
|
|
if (string.IsNullOrWhiteSpace(secret.Name))
|
|
{
|
|
throw new Exception("Must define a secret name when creating new secrets.");
|
|
}
|
|
else if (secret.FolderID <= 0)
|
|
{
|
|
throw new Exception("Must specify a parent folder to place new secrets in.");
|
|
}
|
|
|
|
var json = JsonConvert.SerializeObject(secret);
|
|
var createFolderContentBytes = Encoding.UTF8.GetBytes(json);
|
|
var byteContent = new ByteArrayContent(createFolderContentBytes);
|
|
byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
|
var createResponse = await _httpClient.PostAsync($"{_apiEndpoint}/secrets/", byteContent);
|
|
var responseContent = await createResponse.Content.ReadAsStringAsync();
|
|
if (!createResponse.IsSuccessStatusCode)
|
|
{
|
|
throw new Exception(responseContent);
|
|
}
|
|
|
|
var result = JsonConvert.DeserializeObject<Secret>(responseContent);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a secret.
|
|
/// </summary>
|
|
/// <param name="secret"></param>
|
|
/// <returns></returns>
|
|
public async Task<Secret> UpdateSecret(Secret secret)
|
|
{
|
|
// Do some basic validation to make sure this isn't going to do something undefined.
|
|
if (string.IsNullOrWhiteSpace(secret.Name))
|
|
{
|
|
throw new Exception("Must specify a valid secret name when updating a secret.");
|
|
}
|
|
else if (secret.FolderID <= 0)
|
|
{
|
|
throw new Exception("Secret must have a valid parent folder ID.");
|
|
}
|
|
else if (secret.ID <= 0)
|
|
{
|
|
throw new Exception("Can only update secrets with a valid secret ID");
|
|
}
|
|
|
|
var json = JsonConvert.SerializeObject(secret);
|
|
var createFolderContentBytes = Encoding.UTF8.GetBytes(json);
|
|
var byteContent = new ByteArrayContent(createFolderContentBytes);
|
|
byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
|
var createResponse = await _httpClient.PutAsync($"{_apiEndpoint}/secrets/{secret.ID}", byteContent);
|
|
var responseContent = await createResponse.Content.ReadAsStringAsync();
|
|
var result = JsonConvert.DeserializeObject<Secret>(responseContent);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uploads a file to a secret with the given field name.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to upload the file to.</param>
|
|
/// <param name="field">The field name of the file on the secret</param>
|
|
/// <param name="filename">The name of the file.</param>
|
|
/// <param name="bytes">The byte data of the file.</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> UploadFile(Secret secret, string fieldName, string filename, byte[] bytes)
|
|
{
|
|
var data = new
|
|
{
|
|
fileName = filename,
|
|
fileAttachment = bytes
|
|
};
|
|
|
|
// Secret Server doesn't want the field ID, or a url encoded field name where spaces turn to %20's,
|
|
// It arbitrarily wants field name spaces replaced with dashes.
|
|
// Cheers to three hours I won't get back.
|
|
fieldName = fieldName.Replace(" ", "-");
|
|
|
|
var json = JsonConvert.SerializeObject(data);
|
|
var createFolderContentBytes = Encoding.UTF8.GetBytes(json);
|
|
var byteContent = new ByteArrayContent(createFolderContentBytes);
|
|
byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
|
var createResponse = await _httpClient.PutAsync($"{_apiEndpoint}/secrets/{secret.ID}/fields/{fieldName}", byteContent);
|
|
return createResponse.IsSuccessStatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uploads a file to a secret.
|
|
/// Throws an exception if there is more than one field type on the secret that is a file.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to upload the file to.</param>
|
|
/// <param name="bytes">The bytes of the file.</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> UploadFile(Secret secret, string filename, byte[] bytes)
|
|
{
|
|
var fileFields = secret.Items.Where(item => item.IsFile);
|
|
if (fileFields.Count() > 1)
|
|
{
|
|
throw new Exception("There is more than one field on this secret that is a file. Please specify the field name.");
|
|
}
|
|
var field = fileFields.First();
|
|
|
|
return await UploadFile(secret, field.FieldName, filename, bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uploads a file to a secret given a file path.
|
|
/// Throws an exception if there is more than one field type on the secret that is a file.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to upload the file to.</param>
|
|
/// <param name="bytes">The bytes of the file.</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> UploadFile(Secret secret, string filepath)
|
|
{
|
|
//Secret secret, string filename, byte[] bytes
|
|
if (!File.Exists(filepath))
|
|
{
|
|
throw new FileNotFoundException($"Could not locate file {filepath}");
|
|
}
|
|
|
|
var filename = Path.GetFileName(filepath);
|
|
var bytes = File.ReadAllBytes(filepath);
|
|
return await UploadFile(secret, filename, bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads a file from a secret with the given field name.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to upload the file to.</param>
|
|
/// <param name="field">The field name of the file on the secret</param>
|
|
/// <param name="filename">The name of the file.</param>
|
|
/// <param name="bytes">The byte data of the file.</param>
|
|
/// <returns></returns>
|
|
public async Task<byte[]> DownloadFile(Secret secret, string fieldName)
|
|
{
|
|
fieldName = fieldName.Replace(" ", "-");
|
|
|
|
var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{secret.ID}/fields/{fieldName}");
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
return null;
|
|
}
|
|
var responseContent = await response.Content.ReadAsByteArrayAsync();
|
|
return responseContent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads a file from a secret.
|
|
/// Throws an exception if there is more than one field type on the secret that is a file.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to upload the file to.</param>
|
|
/// <param name="bytes">The bytes of the file.</param>
|
|
/// <returns></returns>
|
|
public async Task<byte[]> DownloadFile(Secret secret)
|
|
{
|
|
var fileFields = secret?.Items.Where(item => item.IsFile);
|
|
if (fileFields == null)
|
|
{
|
|
return null;
|
|
}
|
|
else if (fileFields.Count() > 1)
|
|
{
|
|
throw new Exception("There is more than one field on this secret that is a file. Please specify the field name.");
|
|
}
|
|
var field = fileFields.First();
|
|
|
|
return await DownloadFile(secret, field.FieldName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads a file from a Secret to a given file path.
|
|
/// Throws an exception if there is more than one field type on the secret that is a file.
|
|
/// </summary>
|
|
/// <param name="filepath">The local file location where the file will be stored.</param>
|
|
/// <param name="secret">The secret to download the file from.</param>
|
|
/// <returns>true if a file was successfully downloaded.</returns>
|
|
public async Task<bool> DownloadFile(string filepath, Secret secret)
|
|
{
|
|
var bytes = await DownloadFile(secret);
|
|
if (bytes == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
File.WriteAllBytes(filepath, bytes);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a secret.
|
|
/// </summary>
|
|
/// <param name="secret"></param>
|
|
/// <returns></returns>
|
|
public async Task<bool> DeleteSecret(Secret secret)
|
|
{
|
|
return await DeleteSecret(secret.ID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a secret.
|
|
/// </summary>
|
|
/// <param name="secretId"></param>
|
|
/// <returns></returns>
|
|
public async Task<bool> DeleteSecret(int secretId)
|
|
{
|
|
var response = await _httpClient.DeleteAsync($"{_apiEndpoint}/secrets/{secretId}");
|
|
return response.IsSuccessStatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compares the hash provided to the hash on the specified secret in Secret Server
|
|
/// </summary>
|
|
/// <param name="secretId">Secret to compare against</param>
|
|
/// <param name="md5hash">Local hash to use for comparison</param>
|
|
/// <returns>Returns true if the hashes *DO NOT* match as this indicates a changed secret.</returns>
|
|
public async Task<bool> DetectChanges(int secretId, string md5Hash)
|
|
{
|
|
var remoteHashResult = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{secretId}/fields/file-hash");
|
|
|
|
if (remoteHashResult.IsSuccessStatusCode)
|
|
{
|
|
var remoteHash = remoteHashResult.Content.ReadAsStringAsync().Result;
|
|
// Hashes come back wrapped in quotes. Rip them out.
|
|
remoteHash = remoteHash.Trim('"');
|
|
var comparisonResult = string.Compare(md5Hash, remoteHash, true);
|
|
|
|
if (0 == comparisonResult)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new HttpRequestException("Exception getting file hash from Secret Server: " + remoteHashResult.RequestMessage);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the secret server client.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_httpClient?.Dispose();
|
|
}
|
|
}
|
|
} |