Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Build/Build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ REM restore packages
nuget.exe restore ..\

REM build install zip file
Msbuild.exe ModuleSpecific.targets /p:VisualStudioVersion=14.0;Version=%version%;Configuration=%buildconfig%;TargetFrameworkVersion=v4.5 /t:Install /l:FileLogger,Microsoft.Build.Engine;logfile=logs\Build_%buildconfig%.log;verbosity=diagnostic
Msbuild.exe ModuleSpecific.targets /p:VisualStudioVersion=14.0;Version=%version%;Configuration=%buildconfig%;TargetFrameworkVersion=v4.6 /t:Install /l:FileLogger,Microsoft.Build.Engine;logfile=logs\Build_%buildconfig%.log;verbosity=diagnostic
if ERRORLEVEL 1 goto end

:end
:end
11 changes: 11 additions & 0 deletions Build/ModuleSpecific.targets
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ProjectToBuild Include="..\Reports\SSRS\$(Company).$(ProductName).SSRSReports.csproj" />
<ProjectToBuild Include="..\Parameters\Standard\$(Company).$(ProductName).StandardParameters.csproj" />
<ProjectToBuild Include="..\Parameters\Mobile\$(Company).$(ProductName).MobileParameters.csproj" />
<ProjectToBuild Include="..\Reports\GoogleSheets\$(Company).$(ProductName).GoogleSheetsReports.csproj" />
</ItemGroup>
<ItemGroup>
<MyResourceFiles Include="..\*.ascx" />
Expand Down Expand Up @@ -40,6 +41,10 @@
<MyResourceFiles Include="..\Parameters\Mobile\**\Resources\*.*" />
<MyResourceFiles Include="..\Parameters\Mobile\Settings\App_LocalResources\**\*.resx" />

<MyResourceFiles Include="..\Reports\GoogleSheets\**\*.ascx" />
<MyResourceFiles Include="..\Reports\GoogleSheets\**\*.resx" />
<MyResourceFiles Include="..\Reports\GoogleSheets\Settings\**\*.xml" />

<MyResourceFiles Include="..\Skins\**\*.*" />
<MyResourceFiles Include="..\Repository\**\*.xml" />
<MyResourceFiles Include="..\Resources\**\*.*" />
Expand All @@ -50,6 +55,12 @@
<MyContentFiles Include="..\refs\*.*" />
<MyContentFiles Include="..\LICENSE" />
<MyContentFiles Include="..\packages\Microsoft.ReportViewer.Runtime.WebForms.11.0.3452.0\lib\Microsoft.ReportViewer.WebForms.dll" />
<MyContentFiles Include="..\packages\Google.Apis.1.41.1\lib\net45\Google.Apis.dll" />
<MyContentFiles Include="..\packages\Google.Apis.Auth.1.41.1\lib\net45\Google.Apis.Auth.dll" />
<MyContentFiles Include="..\packages\Google.Apis.Core.1.41.1\lib\net45\Google.Apis.Core.dll" />
<MyContentFiles Include="..\packages\Google.Apis.Drive.v3.1.41.1.1750\lib\net45\Google.Apis.Drive.v3.dll" />
<MyContentFiles Include="..\packages\Google.Apis.Sheets.v4.1.41.1.1754\lib\net45\Google.Apis.Sheets.v4.dll" />
<MyContentFiles Include="..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll" />
</ItemGroup>
<Import Project="DNNInstall.targets" />
</Project>
177 changes: 177 additions & 0 deletions Components/Services/DelineaClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace DNNStuff.SQLViewPro.Services
{
/// <summary>
/// Minimal client for Delinea Secret Server's REST API, used to retrieve secret field
/// values (e.g. the Google service-account JSON key) without storing them in this codebase.
/// Configuration is read from web.config appSettings:
/// DNNStuff:SQLViewPro:DelineaBaseUrl, DNNStuff:SQLViewPro:DelineaUsername,
/// DNNStuff:SQLViewPro:DelineaPassword.
/// </summary>
public class DelineaClient
{
private const int TokenExpiryBufferSeconds = 60;

private static readonly object TokenLock = new object();
private static string _cachedAccessToken;
private static DateTime _cachedTokenExpiresAtUtc = DateTime.MinValue;

private readonly string _baseUrl;
private readonly string _username;
private readonly string _password;

public DelineaClient() : this(
ConfigurationManager.AppSettings["DNNStuff:SQLViewPro:DelineaBaseUrl"],
ConfigurationManager.AppSettings["DNNStuff:SQLViewPro:DelineaUsername"],
ConfigurationManager.AppSettings["DNNStuff:SQLViewPro:DelineaPassword"])
{
}

public DelineaClient(string baseUrl, string username, string password)
{
if (string.IsNullOrEmpty(baseUrl))
{
throw new InvalidOperationException("DNNStuff:SQLViewPro:DelineaBaseUrl is not configured in web.config appSettings.");
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
throw new InvalidOperationException("DNNStuff:SQLViewPro:DelineaUsername / DelineaPassword are not configured in web.config appSettings.");
}

_baseUrl = baseUrl;
_username = username;
_password = password;
}

/// <summary>
/// Looks up a secret by name and returns the value of the field identified by
/// <paramref name="fieldSlug"/> (e.g. "data" for a JSON-key style secret).
/// </summary>
public string GetFieldValue(string secretName, string fieldSlug)
{
return GetFieldValueAsync(secretName, fieldSlug).GetAwaiter().GetResult();
}

public async Task<string> GetFieldValueAsync(string secretName, string fieldSlug)
{
if (string.IsNullOrEmpty(secretName))
{
throw new ArgumentException("secretName must be provided.", "secretName");
}
if (string.IsNullOrEmpty(fieldSlug))
{
throw new ArgumentException("fieldSlug must be provided.", "fieldSlug");
}

var accessToken = await GetAccessTokenAsync().ConfigureAwait(false);

using (var client = CreateHttpClient(accessToken))
{
var secretId = await FindSecretIdByNameAsync(client, secretName).ConfigureAwait(false);
return await GetSecretFieldValueAsync(client, secretId, fieldSlug).ConfigureAwait(false);
}
}

private async Task<string> GetAccessTokenAsync()
{
lock (TokenLock)
{
if (_cachedAccessToken != null && DateTime.UtcNow < _cachedTokenExpiresAtUtc)
{
return _cachedAccessToken;
}
}

using (var client = new HttpClient())
{
var form = new FormUrlEncodedContent(new[]
{
new System.Collections.Generic.KeyValuePair<string, string>("grant_type", "password"),
new System.Collections.Generic.KeyValuePair<string, string>("username", _username),
new System.Collections.Generic.KeyValuePair<string, string>("password", _password)
});

var response = await client.PostAsync(_baseUrl + "/oauth2/token", form).ConfigureAwait(false);
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(string.Format("Delinea token request failed ({0}): {1}", (int)response.StatusCode, body));
}

var json = JObject.Parse(body);
var accessToken = (string)json["access_token"];
var expiresIn = (int?)json["expires_in"] ?? 0;

lock (TokenLock)
{
_cachedAccessToken = accessToken;
_cachedTokenExpiresAtUtc = DateTime.UtcNow.AddSeconds(Math.Max(0, expiresIn - TokenExpiryBufferSeconds));
}

return accessToken;
}
}

private static async Task<int> FindSecretIdByNameAsync(HttpClient client, string secretName)
{
var url = string.Format("/api/v1/secrets?filter.searchText={0}", Uri.EscapeDataString(secretName));
var response = await client.GetAsync(url).ConfigureAwait(false);
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(string.Format("Delinea secret search failed ({0}): {1}", (int)response.StatusCode, body));
}

var json = JObject.Parse(body);
var records = json["records"] as JArray;
if (records == null || records.Count == 0)
{
throw new InvalidOperationException(string.Format("Delinea secret '{0}' was not found.", secretName));
}

return (int)records[0]["id"];
}

private static async Task<string> GetSecretFieldValueAsync(HttpClient client, int secretId, string fieldSlug)
{
var response = await client.GetAsync(string.Format("/api/v1/secrets/{0}", secretId)).ConfigureAwait(false);
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(string.Format("Delinea secret fetch failed ({0}): {1}", (int)response.StatusCode, body));
}

var json = JObject.Parse(body);
var items = json["items"] as JArray;
if (items != null)
{
foreach (var item in items)
{
if (string.Equals((string)item["slug"], fieldSlug, StringComparison.OrdinalIgnoreCase))
{
return (string)item["itemValue"];
}
}
}

throw new InvalidOperationException(string.Format("Delinea secret {0} does not have a field with slug '{1}'.", secretId, fieldSlug));
}

private HttpClient CreateHttpClient(string accessToken)
{
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
return client;
}
}
}
Loading