Skip to content

Commit

Permalink
Admin Approval by webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
sk-keeper committed Feb 24, 2021
1 parent 2fe46de commit 1ebf792
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 87 deletions.
109 changes: 57 additions & 52 deletions AzureAdminAutoApprove/ApproveUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Enterprise;
using Google.Protobuf;
using KeeperSecurity.Authentication;
using KeeperSecurity.Authentication.Sync;
using KeeperSecurity.Commands;
using KeeperSecurity.Configuration;
using KeeperSecurity.Utils;
Expand Down Expand Up @@ -70,7 +71,7 @@ private static async Task<List<KeeperApiResponse>> ExecuteCommands(this IAuthent
return responses;
}

public static async Task ExecuteTeamApprove(Auth auth, ILogger log)
public static async Task ExecuteTeamApprove(AuthCommon auth, ILogger log)
{
var teamRq = new EnterpriseDataCommand
{
Expand Down Expand Up @@ -110,7 +111,7 @@ public static async Task ExecuteTeamApprove(Auth auth, ILogger log)
commands.Add(cmd);
}

var responses = await auth.ExecuteCommands(commands);
await auth.ExecuteCommands(commands);
}

var userRq = new EnterpriseDataCommand
Expand Down Expand Up @@ -208,7 +209,7 @@ public static async Task ExecuteTeamApprove(Auth auth, ILogger log)

var userKeys = new Dictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
var emails = usersToApprove
.Select(x => userLookup.TryGetValue(x, out var u) ? u : null )
.Select(x => userLookup.TryGetValue(x, out var u) ? u : null)
.Where(x => x != null)
.Select(x => x.Username)
.Take(99)
Expand Down Expand Up @@ -285,39 +286,47 @@ public static async Task ExecuteDeviceApprove(IAuthentication auth, IList<string
var rs = await auth.ExecuteAuthCommand<EnterpriseDataCommand, EnterpriseDataResponse>(keysRq);
if ((rs.DeviceRequestForApproval?.Count ?? 0) == 0) return;

var userDataKeys = new Dictionary<long, byte[]>();
var userIds = new HashSet<long>();
foreach (var drq in rs.DeviceRequestForApproval)
{
if (!userDataKeys.ContainsKey(drq.EnterpriseUserId))
if (!userIds.Contains(drq.EnterpriseUserId))
{
userDataKeys[drq.EnterpriseUserId] = null;
userIds.Add(drq.EnterpriseUserId);
}
}

var dataKeyRq = new UserDataKeyRequest();
dataKeyRq.EnterpriseUserId.AddRange(userDataKeys.Keys);
var dataKeyRs = await auth.ExecuteAuthRest<UserDataKeyRequest, EnterpriseUserDataKeys>("enterprise/get_enterprise_user_data_key", dataKeyRq);
foreach (var key in dataKeyRs.Keys)
var userDataKeys = new Dictionary<long, byte[]>();
while (userIds.Count > 0)
{
if (key.UserEncryptedDataKey.IsEmpty) continue;
if (key.KeyTypeId != 2) continue;
try
{
var userDataKey = CryptoUtils.DecryptEc(key.UserEncryptedDataKey.ToByteArray(), _enterprisePrivateKey);
userDataKeys[key.EnterpriseUserId] = userDataKey;
}
catch (Exception e)
var ids = userIds.Take(100).ToArray();
userIds.ExceptWith(ids);

var dataKeyRq = new UserDataKeyRequest();
dataKeyRq.EnterpriseUserId.AddRange(ids);
var dataKeyRs = await auth.ExecuteAuthRest<UserDataKeyRequest, EnterpriseUserDataKeys>("enterprise/get_enterprise_user_data_key", dataKeyRq);
foreach (var key in dataKeyRs.Keys)
{
messages.Add($"Data key decrypt error: {e.Message}");
if (key.UserEncryptedDataKey.IsEmpty) continue;
if (key.KeyTypeId != 2) continue;
try
{
var userDataKey = CryptoUtils.DecryptEc(key.UserEncryptedDataKey.ToByteArray(), _enterprisePrivateKey);
userDataKeys[key.EnterpriseUserId] = userDataKey;
}
catch (Exception e)
{
messages.Add($"Data key decrypt error: {e.Message}");
}
}
}

var approveDevicesRq = new ApproveUserDevicesRequest();

var requests = new Queue<ApproveUserDeviceRequest>();
foreach (var drq in rs.DeviceRequestForApproval)
{
if (!userDataKeys.ContainsKey(drq.EnterpriseUserId) || userDataKeys[drq.EnterpriseUserId] == null) continue;
if (!userDataKeys.TryGetValue(drq.EnterpriseUserId, out var dataKey)) continue;
if (dataKey == null) continue;

var dataKey = userDataKeys[drq.EnterpriseUserId];
var devicePublicKey = CryptoUtils.LoadPublicEcKey(drq.DevicePublicKey.Base64UrlDecode());
var encDataKey = CryptoUtils.EncryptEc(dataKey, devicePublicKey);
var approveRq = new ApproveUserDeviceRequest
Expand All @@ -327,34 +336,47 @@ public static async Task ExecuteDeviceApprove(IAuthentication auth, IList<string
EncryptedDeviceDataKey = ByteString.CopyFrom(encDataKey),
DenyApproval = false,
};
approveDevicesRq.DeviceRequests.Add(approveRq);
requests.Enqueue(approveRq);
}

if (approveDevicesRq.DeviceRequests.Count == 0) return;

var approveRs = await auth.ExecuteAuthRest<ApproveUserDevicesRequest, ApproveUserDevicesResponse>("enterprise/approve_user_devices", approveDevicesRq);
foreach (var deviceRs in approveRs.DeviceResponses)
while (requests.Count > 0)
{
var message = $"Approve device for {deviceRs.EnterpriseUserId} {(deviceRs.Failed ? "failed" : "succeeded")}";
Debug.WriteLine(message);
messages.Add(message);
var approveDevicesRq = new ApproveUserDevicesRequest();
while (requests.Count > 0 && approveDevicesRq.DeviceRequests.Count < 100)
{
approveDevicesRq.DeviceRequests.Add(requests.Dequeue());
}

var approveRs = await auth.ExecuteAuthRest<ApproveUserDevicesRequest, ApproveUserDevicesResponse>("enterprise/approve_user_devices", approveDevicesRq);
foreach (var deviceRs in approveRs.DeviceResponses)
{
var message = $"Approve device for {deviceRs.EnterpriseUserId} {(deviceRs.Failed ? "failed" : "succeeded")}";
Debug.WriteLine(message);
messages.Add(message);
}
}
}

public static async Task<Auth> ConnectToKeeper(ILogger log)
public static async Task<AuthCommon> ConnectToKeeper(ILogger log)
{
if (!await Semaphore.WaitAsync(TimeSpan.FromSeconds(10))) throw new Exception("Timed out");
try
{
var configPath = GetKeeperConfigurationFilePath();
var jsonCache = new JsonConfigurationCache(new JsonConfigurationFileLoader(configPath));
var jsonConfiguration = new JsonConfigurationStorage(jsonCache);
var auth = new Auth(new AuthUiNoAction(), jsonConfiguration)
var auth = new AuthSync(jsonConfiguration)
{
ResumeSession = true
ResumeSession = true,
SupportRestrictedSession = true,
};
await auth.Login(jsonConfiguration.LastLogin);
jsonCache.Flush();
if (auth.Step.State != AuthState.Connected)
{
throw new Exception($"Unexpected login state: {auth.Step.State}. Configuration file may need to be updated.");
}

jsonConfiguration.Flush();

var keysRq = new EnterpriseDataCommand
{
Expand Down Expand Up @@ -384,22 +406,5 @@ public static async Task<Auth> ConnectToKeeper(ILogger log)
}
}
}

internal class AuthUiNoAction : IAuthUI
{
public Task<bool> WaitForDeviceApproval(IDeviceApprovalChannelInfo[] channels, CancellationToken token)
{
return Task.FromResult(false);
}

public Task<bool> WaitForTwoFactorCode(ITwoFactorChannelInfo[] channels, CancellationToken token)
{
return Task.FromResult(false);
}

public Task<bool> WaitForUserPassword(IPasswordInfo passwordInfo, CancellationToken token)
{
return Task.FromResult(false);
}
}
}

103 changes: 78 additions & 25 deletions AzureAdminAutoApprove/AutoApprove.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Web.Http;
using KeeperSecurity.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace AzureAdminAutoApprove
{
public static class AutoApprove
{

[FunctionName("KeeperConnectionGuard")]
public static async Task KeeperConnectionGuard(
[TimerTrigger("12 12 */12 * * *")]
TimerInfo myTimer,
ILogger log)
{
using var auth = await ApproveUtils.ConnectToKeeper(log);
}


private const string AutoApproveWebHookAuthKey = "AutoApproveWebHookAuth";
private const string HttpAuthenticationType = "Bearer";

[FunctionName("ApprovePendingRequestsByWebHook")]
public static async Task<IActionResult> ApprovePendingRequestsByWebHook(
[HttpTrigger(AuthorizationLevel.Function, "POST", Route = null)]
HttpRequest req,
ILogger log)
{

var authHeader = req.Headers["Authorization"];
if (authHeader.Count > 0)
{
var webHookAuth = Environment.GetEnvironmentVariable(AutoApproveWebHookAuthKey);
if (string.IsNullOrEmpty(webHookAuth))
{
log.LogError($"Rejected: Configuration required. Set {AutoApproveWebHookAuthKey} property.");
return new InternalServerErrorResult();
}

var matches = false;
foreach (var authValue in authHeader)
{
log.LogInformation($"Authorization: {authValue}");
if (authValue.StartsWith(HttpAuthenticationType, StringComparison.InvariantCultureIgnoreCase))
{
var token = authValue.Substring(HttpAuthenticationType.Length).Trim();
matches = token == webHookAuth;
if (matches) break;
}
}

if (!matches)
{
log.LogError($"Rejected: Request is not authorized. Ensure {AutoApproveWebHookAuthKey} property matches request Auth token.");
return new UnauthorizedResult();
}
}

var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
if (req.ContentType != "application/json")
{
log.LogWarning($"Expected: \"application/json\" content type. Got: {req.ContentType}");
}

using var auth = await ApproveUtils.ConnectToKeeper(log);
var messages = new List<string>();
await ApproveUtils.ExecuteDeviceApprove(auth, messages);
foreach (var message in messages)
{
log.LogWarning(message);
}

return new OkResult();
}


[FunctionName("ApprovePendingRequestsByTimer")]
public static async Task RunApprovePendingRequests([TimerTrigger("0 */1 * * * *")]
TimerInfo myTimer,
Expand All @@ -19,6 +92,7 @@ public static async Task RunApprovePendingRequests([TimerTrigger("0 */1 * * * *"
var messages = new List<string>();
using var auth = await ApproveUtils.ConnectToKeeper(log);
var approveStep = 0;

bool Callback(NotificationEvent evt)
{
if (string.Compare(evt.Event, "request_device_admin_approval", StringComparison.InvariantCultureIgnoreCase) != 0) return false;
Expand Down Expand Up @@ -47,6 +121,7 @@ bool Callback(NotificationEvent evt)

return false;
}

auth.PushNotifications.RegisterCallback(Callback);

await Task.Delay(TimeSpan.FromSeconds(5));
Expand All @@ -65,7 +140,9 @@ bool Callback(NotificationEvent evt)
}

[FunctionName("ApproveQueuedTeamsByTimer")]
public static async Task RunApproveQueuedTeams([TimerTrigger("0 */10 * * * *")] TimerInfo myTimer, ILogger log)
public static async Task RunApproveQueuedTeams([TimerTrigger("0 */10 * * * *")]
TimerInfo myTimer,
ILogger log)
{
log.LogInformation($"ApproveQueuedTeamsByTimer trigger executed at: {DateTime.Now}");

Expand All @@ -81,29 +158,5 @@ public static async Task RunApproveQueuedTeams([TimerTrigger("0 */10 * * * *")]
}

}
/*
[FunctionName("DumpPendingMessages")]
public static Task<IActionResult> RunDumpPendingMessages(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
HttpRequest req,
ILogger log)
{
log.LogInformation("HTTP trigger: ApprovePendingRequests.");
while (!ApproveUtils.Errors.IsEmpty)
{
if (ApproveUtils.Errors.TryTake(out var message))
{
log.LogInformation(message);
}
else
{
break;
}
}
return Task.FromResult<IActionResult>( new OkObjectResult("Success"));
}
*/
}
}
4 changes: 2 additions & 2 deletions AzureAdminAutoApprove/AzureAdminAutoApprove.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Keeper.Sdk" Version="0.9.7" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.9" />
<PackageReference Include="Keeper.Sdk" Version="0.9.9" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
Expand Down
4 changes: 4 additions & 0 deletions AzureAdminAutoApprove/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Azure Function name is `ApprovePendingRequestsByTimer`. This function is configu

### Web Hook
Azure Function name is `ApprovePendingRequestsByWebHook`. This function requires function level authorization URL.
Azure Function with name `KeeperConnectionGuard` should used along with the `ApprovePendingRequestsByWebHook`.
It just connects to the Keeper server to keep the session alive. It is executed every 12 hours, so make sure
the `Auto Logout` timeout for the Keeper accont is set to at least a day (or 1440 minutes)


### Instructions
See full installation instructions at our documentation portal:
Expand Down
8 changes: 0 additions & 8 deletions AzureAdminAutoApprove/nuget.config

This file was deleted.

0 comments on commit 1ebf792

Please sign in to comment.