Skip to content
This repository was archived by the owner on Jul 30, 2024. It is now read-only.

Commit 352ce8d

Browse files
Merge pull request #657 from NuGet/dev
RI Dev -> Master
2 parents 82843ac + 66d68d1 commit 352ce8d

177 files changed

Lines changed: 837 additions & 613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ArchivePackages/Properties/AssemblyInfo.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
using System.Reflection;
4-
using System.Runtime.CompilerServices;
54
using System.Runtime.InteropServices;
65

76
// General Information about an assembly is controlled through the following

src/Gallery.CredentialExpiration/Configuration/InitializationConfiguration.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@ namespace Gallery.CredentialExpiration
55
{
66
public class InitializationConfiguration
77
{
8-
public int AllowEmailResendAfterDays { get; set; }
9-
108
public string ContainerName { get; set; }
119

1210
public string DataStorageAccount { get; set; }
1311

12+
public string EmailPublisherConnectionString { get; set; }
13+
14+
public string EmailPublisherTopicName { get; set; }
15+
1416
public string GalleryAccountUrl { get; set; }
1517

1618
public string GalleryBrand { get; set; }
1719

1820
public string MailFrom { get; set; }
1921

20-
public string SmtpUri { get; set; }
21-
2222
public int WarnDaysBeforeExpiration { get; set; }
2323

2424
public bool WhatIf { get; set; }
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Mail;
8+
using Gallery.CredentialExpiration.Models;
9+
using NuGet.Services.Messaging.Email;
10+
11+
namespace Gallery.CredentialExpiration
12+
{
13+
public class CredentialExpirationEmailBuilder : MarkdownEmailBuilder
14+
{
15+
public CredentialExpirationEmailBuilder(
16+
InitializationConfiguration initializationConfiguration,
17+
MailAddress sender,
18+
string username,
19+
List<ExpiredCredentialData> credentials,
20+
DateTimeOffset jobRunTime,
21+
bool areCredentialsExpired)
22+
{
23+
InitializationConfiguration = initializationConfiguration
24+
?? throw new ArgumentNullException(nameof(initializationConfiguration));
25+
26+
Sender = sender ?? throw new ArgumentNullException(nameof(sender));
27+
28+
Credentials = credentials
29+
?? throw new ArgumentNullException(nameof(credentials));
30+
31+
if (!Credentials.Any())
32+
{
33+
throw new ArgumentException(
34+
"Must provide at least one expiring or expired credential!",
35+
nameof(credentials));
36+
}
37+
38+
JobRunTime = jobRunTime;
39+
AreCredentialsExpired = areCredentialsExpired;
40+
41+
Username = username ?? throw new ArgumentNullException(nameof(username));
42+
var userEmail = credentials.FirstOrDefault()?.EmailAddress
43+
?? throw new ArgumentException(
44+
"Credentials provided must have an email address!",
45+
nameof(credentials));
46+
47+
UserAddress = new MailAddress(userEmail, Username);
48+
}
49+
50+
public InitializationConfiguration InitializationConfiguration { get; }
51+
52+
public string Username { get; }
53+
public MailAddress UserAddress { get; }
54+
public List<ExpiredCredentialData> Credentials { get; }
55+
public DateTimeOffset JobRunTime { get; }
56+
public bool AreCredentialsExpired { get; }
57+
58+
public override MailAddress Sender { get; }
59+
60+
public override IEmailRecipients GetRecipients()
61+
{
62+
return new EmailRecipients(
63+
to: new MailAddress[] { UserAddress });
64+
}
65+
66+
public override string GetSubject()
67+
{
68+
var template = AreCredentialsExpired ? Strings.ExpiredEmailSubject : Strings.ExpiringEmailSubject;
69+
return string.Format(template, InitializationConfiguration.GalleryBrand);
70+
}
71+
72+
protected override string GetMarkdownBody()
73+
{
74+
var apiKeyExpiryMessageList = Credentials
75+
.Select(x => BuildApiKeyExpiryMessage(x.Description, x.Expires, JobRunTime))
76+
.ToList();
77+
78+
var apiKeyExpiryMessage = string.Join(Environment.NewLine, apiKeyExpiryMessageList);
79+
var template = AreCredentialsExpired ? Strings.ExpiredEmailBody : Strings.ExpiringEmailBody;
80+
return string.Format(template, Username, InitializationConfiguration.GalleryBrand, apiKeyExpiryMessage, InitializationConfiguration.GalleryAccountUrl);
81+
}
82+
83+
private static string BuildApiKeyExpiryMessage(string description, DateTimeOffset expiry, DateTimeOffset currentTime)
84+
{
85+
var expiryInDays = (expiry - currentTime).TotalDays;
86+
var message = expiryInDays < 0
87+
? string.Format(Strings.ApiKeyExpired, description)
88+
: string.Format(Strings.ApiKeyExpiring, description, (int)expiryInDays);
89+
90+
// \u2022 - Unicode for bullet point.
91+
return "\u2022 " + message + Environment.NewLine;
92+
}
93+
}
94+
}

src/Gallery.CredentialExpiration/Gallery.CredentialExpiration.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
</ItemGroup>
4343
<ItemGroup>
4444
<Compile Include="Configuration\InitializationConfiguration.cs" />
45+
<Compile Include="CredentialExpirationEmailBuilder.cs" />
4546
<Compile Include="GalleryCredentialExpiration.cs" />
4647
<Compile Include="ICredentialExpirationExporter.cs" />
4748
<Compile Include="JobRunTimeCursor.cs" />
@@ -101,6 +102,9 @@
101102
<PackageReference Include="Newtonsoft.Json">
102103
<Version>9.0.1</Version>
103104
</PackageReference>
105+
<PackageReference Include="NuGet.Services.Messaging.Email">
106+
<Version>2.33.0</Version>
107+
</PackageReference>
104108
<PackageReference Include="NuGet.Services.Storage">
105109
<Version>2.1.3</Version>
106110
</PackageReference>

src/Gallery.CredentialExpiration/Job.cs

Lines changed: 35 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.ComponentModel.Design;
77
using System.Linq;
8-
using System.Net;
98
using System.Net.Mail;
109
using System.Threading;
1110
using System.Threading.Tasks;
@@ -18,6 +17,9 @@
1817
using Microsoft.WindowsAzure.Storage;
1918
using Newtonsoft.Json;
2019
using NuGet.Jobs;
20+
using NuGet.Services.Messaging;
21+
using NuGet.Services.Messaging.Email;
22+
using NuGet.Services.ServiceBus;
2123
using NuGet.Services.Storage;
2224

2325
namespace Gallery.CredentialExpiration
@@ -28,22 +30,27 @@ public class Job : JsonConfigurationJob
2830

2931
private readonly string _cursorFile = "cursorv2.json";
3032

31-
private InitializationConfiguration Configuration { get; set; }
33+
private InitializationConfiguration InitializationConfiguration { get; set; }
34+
private MailAddress FromAddress { get; set; }
35+
private AsynchronousEmailMessageService EmailService { get; set; }
3236

3337
private Storage Storage { get; set; }
3438

35-
private SmtpClient SmtpClient { get; set; }
36-
3739
public override void Init(IServiceContainer serviceContainer, IDictionary<string, string> jobArgsDictionary)
3840
{
3941
base.Init(serviceContainer, jobArgsDictionary);
4042

41-
Configuration = _serviceProvider.GetRequiredService<IOptionsSnapshot<InitializationConfiguration>>().Value;
43+
InitializationConfiguration = _serviceProvider.GetRequiredService<IOptionsSnapshot<InitializationConfiguration>>().Value;
44+
45+
var serializer = new ServiceBusMessageSerializer();
46+
var topicClient = new TopicClientWrapper(InitializationConfiguration.EmailPublisherConnectionString, InitializationConfiguration.EmailPublisherTopicName);
47+
var enqueuer = new EmailMessageEnqueuer(topicClient, serializer, LoggerFactory.CreateLogger<EmailMessageEnqueuer>());
48+
EmailService = new AsynchronousEmailMessageService(enqueuer);
4249

43-
SmtpClient = CreateSmtpClient(Configuration.SmtpUri);
50+
FromAddress = new MailAddress(InitializationConfiguration.MailFrom);
4451

45-
var storageAccount = CloudStorageAccount.Parse(Configuration.DataStorageAccount);
46-
var storageFactory = new AzureStorageFactory(storageAccount, Configuration.ContainerName, LoggerFactory);
52+
var storageAccount = CloudStorageAccount.Parse(InitializationConfiguration.DataStorageAccount);
53+
var storageFactory = new AzureStorageFactory(storageAccount, InitializationConfiguration.ContainerName, LoggerFactory);
4754
Storage = storageFactory.Create();
4855
}
4956

@@ -53,7 +60,7 @@ public override async Task Run()
5360
// Default values
5461
var jobCursor = new JobRunTimeCursor( jobCursorTime: jobRunTime, maxProcessedCredentialsTime: jobRunTime );
5562
var galleryCredentialExpiration = new GalleryCredentialExpiration(this,
56-
new CredentialExpirationJobMetadata(jobRunTime, Configuration.WarnDaysBeforeExpiration, jobCursor));
63+
new CredentialExpirationJobMetadata(jobRunTime, InitializationConfiguration.WarnDaysBeforeExpiration, jobCursor));
5764

5865
try
5966
{
@@ -70,7 +77,7 @@ public override async Task Run()
7077
new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error });
7178

7279
galleryCredentialExpiration = new GalleryCredentialExpiration(this,
73-
new CredentialExpirationJobMetadata(jobRunTime, Configuration.WarnDaysBeforeExpiration, jobCursor));
80+
new CredentialExpirationJobMetadata(jobRunTime, InitializationConfiguration.WarnDaysBeforeExpiration, jobCursor));
7481
}
7582

7683
// Connect to database
@@ -86,11 +93,11 @@ public override async Task Run()
8693
.ForEach(ecd => ecd.Description = Constants.NonScopedApiKeyDescription);
8794

8895
// Group credentials for each user
89-
var userToExpiredCredsMapping = credentialsInRange
96+
var userToCredentialsMapping = credentialsInRange
9097
.GroupBy(x => x.Username)
9198
.ToDictionary(user => user.Key, value => value.ToList());
9299

93-
foreach (var userCredMapping in userToExpiredCredsMapping)
100+
foreach (var userCredMapping in userToCredentialsMapping)
94101
{
95102
var username = userCredMapping.Key;
96103
var credentialList = userCredMapping.Value;
@@ -99,10 +106,10 @@ public override async Task Run()
99106
var expiringCredentialList = galleryCredentialExpiration.GetExpiringCredentials(credentialList);
100107
var expiredCredentialList = galleryCredentialExpiration.GetExpiredCredentials(credentialList);
101108

102-
await HandleExpiredCredentialEmail(username, expiringCredentialList, jobRunTime, expired: false);
109+
await HandleExpiredCredentialEmail(username, expiringCredentialList, jobRunTime, areCredentialsExpired: false);
103110

104111
// send expired API keys email notification
105-
await HandleExpiredCredentialEmail(username, expiredCredentialList, jobRunTime, expired: true);
112+
await HandleExpiredCredentialEmail(username, expiredCredentialList, jobRunTime, areCredentialsExpired: true);
106113
}
107114
}
108115
finally
@@ -117,53 +124,35 @@ public override async Task Run()
117124
}
118125
}
119126

120-
private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCredentialData> credentialList, DateTimeOffset jobRunTime, bool expired)
127+
private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCredentialData> credentials, DateTimeOffset jobRunTime, bool areCredentialsExpired)
121128
{
122-
if (credentialList == null || credentialList.Count == 0)
129+
if (credentials == null || credentials.Count == 0)
123130
{
124131
return;
125132
}
126133

127134
Logger.LogInformation("Handling {Expired} credential(s) (Keys: {Descriptions})...",
128-
expired ? "expired" : "expiring",
129-
string.Join(", ", credentialList.Select(x => x.Description).ToList()));
130-
131-
// Build message
132-
var userEmail = credentialList.FirstOrDefault().EmailAddress;
133-
var mailMessage = new MailMessage(Configuration.MailFrom, userEmail);
135+
areCredentialsExpired ? "expired" : "expiring",
136+
string.Join(", ", credentials.Select(x => x.Description).ToList()));
134137

135-
var apiKeyExpiryMessageList = credentialList
136-
.Select(x => BuildApiKeyExpiryMessage(x.Description, x.Expires, jobRunTime))
137-
.ToList();
138-
139-
var apiKeyExpiryMessage = string.Join(Environment.NewLine, apiKeyExpiryMessageList);
140-
// Build email body
141-
if (expired)
142-
{
143-
mailMessage.Subject = string.Format(Strings.ExpiredEmailSubject, Configuration.GalleryBrand);
144-
mailMessage.Body = string.Format(Strings.ExpiredEmailBody, username, Configuration.GalleryBrand, apiKeyExpiryMessage, Configuration.GalleryAccountUrl);
145-
}
146-
else
147-
{
148-
mailMessage.Subject = string.Format(Strings.ExpiringEmailSubject, Configuration.GalleryBrand);
149-
mailMessage.Body = string.Format(Strings.ExpiringEmailBody, username, Configuration.GalleryBrand, apiKeyExpiryMessage, Configuration.GalleryAccountUrl);
150-
}
138+
var emailBuilder = new CredentialExpirationEmailBuilder(
139+
InitializationConfiguration,
140+
FromAddress,
141+
username,
142+
credentials,
143+
jobRunTime,
144+
areCredentialsExpired);
151145

152146
// Send email
153147
try
154148
{
155-
if (!Configuration.WhatIf) // if WhatIf is passed, we will not send e-mails (e.g. dev/int don't have to annoy users)
149+
if (!InitializationConfiguration.WhatIf) // if WhatIf is passed, we will not send e-mails (e.g. dev/int don't have to annoy users)
156150
{
157-
await SmtpClient.SendMailAsync(mailMessage);
151+
await EmailService.SendMessageAsync(emailBuilder);
158152
}
159153

160154
Logger.LogInformation("Handled {Expired} credential .",
161-
expired ? "expired" : "expiring");
162-
}
163-
catch (SmtpFailedRecipientException ex)
164-
{
165-
var logMessage = "Failed to handle credential - recipient failed!";
166-
Logger.LogWarning(LogEvents.FailedToSendMail, ex, logMessage);
155+
areCredentialsExpired ? "expired" : "expiring");
167156
}
168157
catch (Exception ex)
169158
{
@@ -174,36 +163,6 @@ private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCre
174163
}
175164
}
176165

177-
private static string BuildApiKeyExpiryMessage(string description, DateTimeOffset expiry, DateTimeOffset currentTime)
178-
{
179-
var expiryInDays = (expiry - currentTime).TotalDays;
180-
var message = expiryInDays < 0
181-
? string.Format(Strings.ApiKeyExpired, description)
182-
: string.Format(Strings.ApiKeyExpiring, description, (int)expiryInDays);
183-
184-
// \u2022 - Unicode for bullet point.
185-
return "\u2022 " + message + Environment.NewLine;
186-
}
187-
188-
private SmtpClient CreateSmtpClient(string smtpUriString)
189-
{
190-
var smtpUri = new SmtpUri(new Uri(smtpUriString));
191-
var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port)
192-
{
193-
EnableSsl = smtpUri.Secure
194-
};
195-
196-
if (!string.IsNullOrWhiteSpace(smtpUri.UserName))
197-
{
198-
smtpClient.UseDefaultCredentials = false;
199-
smtpClient.Credentials = new NetworkCredential(
200-
smtpUri.UserName,
201-
smtpUri.Password);
202-
}
203-
204-
return smtpClient;
205-
}
206-
207166
protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder)
208167
{
209168
}

src/Gallery.CredentialExpiration/Settings/dev.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"Initialization": {
3-
"AllowEmailResendAfterDays": 7,
43
"ContainerName": "credentialexpiration",
54
"DataStorageAccount": "DefaultEndpointsProtocol=https;AccountName=nugetdevuse2gallery;AccountKey=$$Dev-NuGetDevUse2Gallery-StorageKey$$",
5+
"EmailPublisherConnectionString": "Endpoint=sb://nugetdev.servicebus.windows.net/;SharedAccessKeyName=enqueuer;SharedAccessKey=$$Dev-ServiceBus-SharedAccessKey-EmailPublisher-Enqueuer$$",
6+
"EmailPublisherTopicName": "email-publisher",
67
"GalleryAccountUrl": "https://dev.nugettest.org/account/ApiKeys",
78
"GalleryBrand": "NuGet Gallery",
89
"MailFrom": "[email protected]",
9-
"SmtpUri": "#{Jobs.gallery.credentialexpiration.SmtpUri}",
1010
"WarnDaysBeforeExpiration": 7,
1111
"WhatIf": true
1212
},

src/Gallery.CredentialExpiration/Settings/int.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"Initialization": {
3-
"AllowEmailResendAfterDays": 7,
43
"ContainerName": "credentialexpiration",
54
"DataStorageAccount": "DefaultEndpointsProtocol=https;AccountName=nugetintusncgallery;AccountKey=$$Int-NuGetIntUsncGallery-StorageKey$$",
5+
"EmailPublisherConnectionString": "Endpoint=sb://nugetint.servicebus.windows.net/;SharedAccessKeyName=enqueuer;SharedAccessKey=$$Int-ServiceBus-SharedAccessKey-EmailPublisher-Enqueuer$$",
6+
"EmailPublisherTopicName": "email-publisher",
67
"GalleryAccountUrl": "https://int.nugettest.org/account/ApiKeys",
78
"GalleryBrand": "NuGet Gallery",
89
"MailFrom": "[email protected]",

src/Gallery.CredentialExpiration/Settings/prod.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"Initialization": {
3-
"AllowEmailResendAfterDays": 7,
43
"ContainerName": "credentialexpiration",
54
"DataStorageAccount": "DefaultEndpointsProtocol=https;AccountName=nugetgallery;AccountKey=$$Prod-NuGetGalleryStorage-Key$$",
5+
"EmailPublisherConnectionString": "Endpoint=sb://nugetprod.servicebus.windows.net/;SharedAccessKeyName=enqueuer;SharedAccessKey=$$Prod-ServiceBus-SharedAccessKey-EmailPublisher-Enqueuer$$",
6+
"EmailPublisherTopicName": "email-publisher",
67
"GalleryAccountUrl": "https://www.nuget.org/account/ApiKeys",
78
"GalleryBrand": "NuGet Gallery",
89
"MailFrom": "[email protected]",

0 commit comments

Comments
 (0)