Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.
/ NuGet.Jobs Public archive

Commit

Permalink
Merge branch 'dev' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ryuyu authored Feb 24, 2017
2 parents ada7125 + b5c8139 commit 31c4e6c
Show file tree
Hide file tree
Showing 69 changed files with 4,268 additions and 310 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ nuget.exe
*.suo
*.user
*.sln.docstates
.vs/config/applicationhost.config

# Build results
[Dd]ebug/
Expand Down Expand Up @@ -184,6 +185,7 @@ UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.jfm

# Business Intelligence projects
*.rdl.data
Expand All @@ -196,4 +198,4 @@ FakesAssemblies/

# Vs2015
.vs/config/applicationhost.config
*.jfm
project.lock.json
9 changes: 9 additions & 0 deletions NuGet.Jobs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gallery", "Gallery", "{8872
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.CredentialExpiration", "src\Gallery.CredentialExpiration\Gallery.CredentialExpiration.csproj", "{FA8C7905-985F-4919-AAA9-4B9A252F4977}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SupportRequests", "SupportRequests", "{BEC3DF4D-9A04-42C8-8B4F-D42750202B4D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.SupportRequests.Notifications", "src\NuGet.SupportRequests.Notifications\NuGet.SupportRequests.Notifications.csproj", "{12719498-B87E-4E92-8C2B-30046393CF85}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -179,6 +183,10 @@ Global
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Release|Any CPU.Build.0 = Release|Any CPU
{12719498-B87E-4E92-8C2B-30046393CF85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12719498-B87E-4E92-8C2B-30046393CF85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12719498-B87E-4E92-8C2B-30046393CF85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12719498-B87E-4E92-8C2B-30046393CF85}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -208,5 +216,6 @@ Global
{185EF6D4-2172-40B1-A80E-811CE9D85840} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
{1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
{FA8C7905-985F-4919-AAA9-4B9A252F4977} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7}
{12719498-B87E-4E92-8C2B-30046393CF85} = {BEC3DF4D-9A04-42C8-8B4F-D42750202B4D}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="LogEvents.cs" />
<Compile Include="MyJobArgumentNames.cs" />
<Compile Include="Models\ExpiredCredentialData.cs" />
<Compile Include="Job.cs" />
Expand Down
108 changes: 82 additions & 26 deletions src/Gallery.CredentialExpiration/Job.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public class Job : JobBase
private string _mailFrom;
private SmtpClient _smtpClient;

private int _allowEmailResendAfterDays = 7;
private int _warnDaysBeforeExpiration = 10;

private ILogger _logger;

public override bool Init(IDictionary<string, string> jobArgsDictionary)
Expand Down Expand Up @@ -64,15 +65,15 @@ public override bool Init(IDictionary<string, string> jobArgsDictionary)
var smtpUri = new SmtpUri(new Uri(smtpConnectionString));
_smtpClient = CreateSmtpClient(smtpUri);

var temp = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.WarnDaysBeforeExpiration);
if (temp.HasValue)
{
_warnDaysBeforeExpiration = temp.Value;
}
_warnDaysBeforeExpiration = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.WarnDaysBeforeExpiration)
?? _warnDaysBeforeExpiration;

_allowEmailResendAfterDays = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.AllowEmailResendAfterDays)
?? _allowEmailResendAfterDays;
}
catch (Exception exception)
{
_logger.LogCritical("Failed to initialize job! {Exception}", exception);
_logger.LogCritical(LogEvents.JobInitFailed, exception, "Failed to initialize job!");

return false;
}
Expand All @@ -93,8 +94,8 @@ public override async Task<bool> Run()
var contactedUsers = JsonConvert.DeserializeObject<Dictionary<string, DateTimeOffset>>(
File.ReadAllText(_cursorFile));

// Clean older entries (contacted in last _warnDaysBeforeExpiration * 2 days)
var referenceDate = DateTimeOffset.UtcNow.AddDays(-2 * _warnDaysBeforeExpiration);
// Clean older entries (contacted in last _allowEmailResendAfterDays)
var referenceDate = DateTimeOffset.UtcNow.AddDays(-1 * _allowEmailResendAfterDays);
foreach (var kvp in contactedUsers.Where(kvp => kvp.Value >= referenceDate))
{
_contactedUsers.AddOrUpdate(kvp.Key, kvp.Value, (s, offset) => kvp.Value);
Expand All @@ -118,24 +119,51 @@ public override async Task<bool> Run()
expiredCredentials.Count);
}

// Add default description for non-scoped API keys
expiredCredentials
.Where(cred => string.IsNullOrEmpty(cred.Description))
.ToList()
.ForEach(ecd => ecd.Description = Constants.NonScopedApiKeyDescription);

// Group credentials for each user
var userToExpiredCredsMapping = expiredCredentials
.GroupBy(x => x.Username)
.ToDictionary(user => user.Key, value => value.ToList());

// Handle expiring credentials
var jobRunTime = DateTimeOffset.UtcNow;
foreach (var expiredCredential in expiredCredentials)
foreach (var userCredMapping in userToExpiredCredsMapping)
{
if (!_contactedUsers.ContainsKey(expiredCredential.Username))
var username = userCredMapping.Key;
var credentialList = userCredMapping.Value;

// Split credentials into two lists: Expired and Expiring to aggregate messages
var expiringCredentialList = credentialList
.Where(x => (x.Expires - jobRunTime).TotalDays > 0)
.ToList();
var expiredCredentialList = credentialList
.Where(x => (x.Expires - jobRunTime).TotalDays <= 0)
.ToList();

DateTimeOffset userContactTime;
if (!_contactedUsers.TryGetValue(username, out userContactTime))
{
await HandleExpiredCredentialEmail(expiredCredential, jobRunTime);
// send expiring API keys email notification
await HandleExpiredCredentialEmail(username, expiringCredentialList, jobRunTime, expired: false);

// send expired API keys email notification
await HandleExpiredCredentialEmail(username, expiredCredentialList, jobRunTime, expired: true);
}
else
{
_logger.LogDebug("Skipping expired credential for user {Username} - already handled today.",
expiredCredential.Username);
_logger.LogDebug("Skipping expired credential for user {Username} - already handled at {JobRuntime}.",
username, userContactTime);
}
}
}
catch (Exception ex)
{
_logger.LogCritical("Job run failed! {Exception}", ex);
_logger.LogCritical(LogEvents.JobRunFailed, ex, "Job run failed!");

return false;
}
Expand All @@ -149,24 +177,37 @@ public override async Task<bool> Run()
return true;
}

private async Task HandleExpiredCredentialEmail(ExpiredCredentialData expiredCredential, DateTimeOffset jobRunTime)
private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCredentialData> credentialList, DateTimeOffset jobRunTime, bool expired)
{
_logger.LogInformation("Handling expired credential for user {Username} (expires: {Expires})...", expiredCredential.Username, expiredCredential.Expires);
if (credentialList == null || credentialList.Count == 0)
{
return;
}

_logger.LogInformation("Handling {Expired} credential(s) for user {Username} (Keys: {Descriptions})...",
expired ? "expired" : "expiring",
username,
string.Join(", ", credentialList.Select(x => x.Description).ToList()));

// Build message
var mailMessage = new MailMessage(_mailFrom, expiredCredential.EmailAddress);
var userEmail = credentialList.FirstOrDefault().EmailAddress;
var mailMessage = new MailMessage(_mailFrom, userEmail);

var apiKeyExpiryMessageList = credentialList
.Select(x => BuildApiKeyExpiryMessage(x.Description, x.Expires, jobRunTime))
.ToList();

var apiKeyExpiryMessage = string.Join(Environment.NewLine, apiKeyExpiryMessageList);
// Build email body
var expiresInDays = expiredCredential.Expires.UtcDateTime - DateTime.UtcNow;
if (expiresInDays.TotalDays <= 0)
if (expired)
{
mailMessage.Subject = string.Format(Strings.ExpiredEmailSubject, _galleryBrand);
mailMessage.Body = string.Format(Strings.ExpiredEmailBody, _galleryBrand, _galleryAccountUrl);
mailMessage.Body = string.Format(Strings.ExpiredEmailBody, username, _galleryBrand, apiKeyExpiryMessage, _galleryAccountUrl);
}
else
{
mailMessage.Subject = string.Format(Strings.ExpiringEmailSubject, _galleryBrand);
mailMessage.Body = string.Format(Strings.ExpiringEmailBody, _galleryBrand, _galleryAccountUrl, (int)expiresInDays.TotalDays);
mailMessage.Body = string.Format(Strings.ExpiringEmailBody, username, _galleryBrand, apiKeyExpiryMessage, _galleryAccountUrl);
}

// Send email
Expand All @@ -177,22 +218,37 @@ private async Task HandleExpiredCredentialEmail(ExpiredCredentialData expiredCre
await _smtpClient.SendMailAsync(mailMessage);
}

_logger.LogInformation("Handled expired credential for user {Username}.", expiredCredential.Username);
_logger.LogInformation("Handled {Expired} credential for user {Username}.",
expired ? "expired" : "expiring",
username);

_contactedUsers.AddOrUpdate(expiredCredential.Username, jobRunTime, (s, offset) => jobRunTime);
_contactedUsers.AddOrUpdate(username, jobRunTime, (s, offset) => jobRunTime);
}
catch (SmtpFailedRecipientException ex)
{
_logger.LogWarning("Failed to handle expired credential for user {Username} - recipient failed.", expiredCredential.Username, ex);
var logMessage = $"Failed to handle credential for user {username} - recipient failed!";
_logger.LogWarning(LogEvents.FailedToSendMail, ex, logMessage);
}
catch (Exception ex)
{
_logger.LogCritical("Failed to handle expired credential for user {Username}.", expiredCredential.Username, ex);
var logMessage = $"Failed to handle credential for user {username}.";
_logger.LogCritical(LogEvents.FailedToHandleExpiredCredential, ex, logMessage);

throw;
}
}

private static string BuildApiKeyExpiryMessage(string description, DateTimeOffset expiry, DateTimeOffset currentTime)
{
var expiryInDays = (expiry - currentTime).TotalDays;
var message = expiryInDays < 0
? string.Format(Strings.ApiKeyExpired, description)
: string.Format(Strings.ApiKeyExpiring, description, (int)expiryInDays);

// \u2022 - Unicode for bullet point.
return "\u2022 "+ message + Environment.NewLine;
}

private SmtpClient CreateSmtpClient(SmtpUri smtpUri)
{
var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port)
Expand Down
15 changes: 15 additions & 0 deletions src/Gallery.CredentialExpiration/LogEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.Extensions.Logging;

namespace Gallery.CredentialExpiration
{
public class LogEvents
{
public static EventId FailedToHandleExpiredCredential = new EventId(600, "Failed to handle expired credential");
public static EventId FailedToSendMail = new EventId(601, "Failed to deliver email");
public static EventId JobRunFailed = new EventId(650, "Job run failed");
public static EventId JobInitFailed = new EventId(651, "Job initialization failed");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@

namespace Gallery.CredentialExpiration.Models
{
public static class Constants
{
public const string NonScopedApiKeyDescription = "Full access API key";
}

public class ExpiredCredentialData
{
public string Type { get; set; }
public string Username { get; set; }
public string EmailAddress { get; set; }
public string Description { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Expires { get; set; }
}
Expand Down
1 change: 1 addition & 0 deletions src/Gallery.CredentialExpiration/MyJobArgumentNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public class MyJobArgumentNames
public const string GalleryBrand = "GalleryBrand";
public const string GalleryAccountUrl = "GalleryAccountUrl";
public const string WarnDaysBeforeExpiration = "WarnDaysBeforeExpiration";
public const string AllowEmailResendAfterDays = "AllowEmailResendAfterDays";
}
}
44 changes: 33 additions & 11 deletions src/Gallery.CredentialExpiration/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 31c4e6c

Please sign in to comment.