Skip to content

Commit 67726d1

Browse files
committed
Implementation of system level keys
1 parent 5b9aba4 commit 67726d1

28 files changed

+438
-123
lines changed

src/WebJobs.Script.WebHost/App_Data/secrets/host.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"value": "zlnu496ve212kk1p84ncrtdvmtpembduqp25ajjc",
1111
"encrypted": false
1212
}
13+
],
14+
"systemKeys": [
15+
{
16+
"name": "samplesystemkey",
17+
"value": "bcnu496ve212kk1p84ncrtdvmtpembduqp25aghe",
18+
"encrypted": false
19+
}
1320
]
14-
15-
}
21+
}

src/WebJobs.Script.WebHost/Controllers/KeysController.cs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class KeysController : ApiController
2020
{
2121
private const string MasterKeyName = "_master";
2222

23+
private static readonly Lazy<Dictionary<string, string>> EmptyKeys = new Lazy<Dictionary<string, string>>(() => new Dictionary<string, string>());
2324
private readonly WebScriptHostManager _scriptHostManager;
2425
private readonly ISecretManager _secretManager;
2526
private readonly TraceWriter _traceWriter;
@@ -45,39 +46,54 @@ public async Task<IHttpActionResult> Get(string name)
4546
}
4647

4748
[HttpGet]
48-
[Route("admin/host/keys")]
49+
[Route("admin/host/{keys:regex(^(keys|functionkeys|systemkeys)$)}")]
4950
public async Task<IHttpActionResult> Get()
5051
{
51-
var hostSecrets = await _secretManager.GetHostSecretsAsync();
52-
return GetKeysResult(hostSecrets.FunctionKeys);
52+
string hostKeyScope = GetHostKeyScopeForRequest();
53+
54+
Dictionary<string, string> keys = await GetHostSecretsByScope(hostKeyScope);
55+
return GetKeysResult(keys);
5356
}
5457

5558
[HttpPost]
5659
[Route("admin/functions/{name}/keys/{keyName}")]
57-
public Task<IHttpActionResult> Post(string name, string keyName) => AddOrUpdateFunctionSecretAsync(keyName, null, name);
60+
public Task<IHttpActionResult> Post(string name, string keyName) => AddOrUpdateSecretAsync(keyName, null, name, ScriptSecretsType.Function);
5861

5962
[HttpPost]
60-
[Route("admin/host/keys/{keyName}")]
61-
public Task<IHttpActionResult> Post(string keyName) => AddOrUpdateFunctionSecretAsync(keyName, null);
63+
[Route("admin/host/{keys:regex(^(keys|functionkeys|systemkeys)$)}/{keyName}")]
64+
public Task<IHttpActionResult> Post(string keyName) => AddOrUpdateSecretAsync(keyName, null, GetHostKeyScopeForRequest(), ScriptSecretsType.Host);
6265

6366
[HttpPut]
6467
[Route("admin/functions/{name}/keys/{keyName}")]
65-
public Task<IHttpActionResult> Put(string name, string keyName, Key key) => PutKeyAsync(keyName, key, name);
68+
public Task<IHttpActionResult> Put(string name, string keyName, Key key) => PutKeyAsync(keyName, key, name, ScriptSecretsType.Function);
6669

6770
[HttpPut]
68-
[Route("admin/host/keys/{keyName}")]
69-
public Task<IHttpActionResult> Put(string keyName, Key key) => PutKeyAsync(keyName, key);
71+
[Route("admin/host/{keys:regex(^(keys|functionkeys|systemkeys)$)}/{keyName}")]
72+
public Task<IHttpActionResult> Put(string keyName, Key key) => PutKeyAsync(keyName, key, GetHostKeyScopeForRequest(), ScriptSecretsType.Host);
7073

7174
[HttpDelete]
7275
[Route("admin/functions/{name}/keys/{keyName}")]
73-
public Task<IHttpActionResult> Delete(string name, string keyName) => DeleteFunctionSecretAsync(keyName, name);
76+
public Task<IHttpActionResult> Delete(string name, string keyName) => DeleteFunctionSecretAsync(keyName, name, ScriptSecretsType.Function);
7477

7578
[HttpDelete]
76-
[Route("admin/host/keys/{keyName}")]
77-
public Task<IHttpActionResult> Delete(string keyName) => DeleteFunctionSecretAsync(keyName);
79+
[Route("admin/host/{keys:regex(^(keys|functionkeys|systemkeys)$)}/{keyName}")]
80+
public Task<IHttpActionResult> Delete(string keyName) => DeleteFunctionSecretAsync(keyName, GetHostKeyScopeForRequest(), ScriptSecretsType.Host);
81+
82+
private string GetHostKeyScopeForRequest()
83+
{
84+
string keyScope = ControllerContext.RouteData.Values.GetValueOrDefault<string>("keys");
85+
86+
if (string.Equals(keyScope, "keys", StringComparison.OrdinalIgnoreCase))
87+
{
88+
keyScope = HostKeyScopes.FunctionKeys;
89+
}
90+
91+
return keyScope;
92+
}
7893

7994
private IHttpActionResult GetKeysResult(IDictionary<string, string> keys)
8095
{
96+
keys = keys ?? EmptyKeys.Value;
8197
var keysContent = new
8298
{
8399
keys = keys.Select(k => new { name = k.Key, value = k.Value })
@@ -88,35 +104,34 @@ private IHttpActionResult GetKeysResult(IDictionary<string, string> keys)
88104
return Ok(keyResponse);
89105
}
90106

91-
private async Task<IHttpActionResult> PutKeyAsync(string keyName, Key key, string functionName = null)
107+
private async Task<IHttpActionResult> PutKeyAsync(string keyName, Key key, string keyScope, ScriptSecretsType secretsType)
92108
{
93109
if (key?.Value == null)
94110
{
95111
return BadRequest("Invalid key value");
96112
}
97113

98-
return await AddOrUpdateFunctionSecretAsync(keyName, key.Value, functionName);
114+
return await AddOrUpdateSecretAsync(keyName, key.Value, keyScope, secretsType);
99115
}
100116

101-
private async Task<IHttpActionResult> AddOrUpdateFunctionSecretAsync(string keyName, string value, string functionName = null)
117+
private async Task<IHttpActionResult> AddOrUpdateSecretAsync(string keyName, string value, string keyScope, ScriptSecretsType secretsType)
102118
{
103-
if (functionName != null &&
104-
!_scriptHostManager.Instance.IsFunction(functionName))
119+
if (secretsType == ScriptSecretsType.Function && keyScope != null && !_scriptHostManager.Instance.IsFunction(keyScope))
105120
{
106121
return NotFound();
107122
}
108123

109124
KeyOperationResult operationResult;
110-
if (functionName == null && string.Equals(keyName, MasterKeyName, StringComparison.OrdinalIgnoreCase))
125+
if (secretsType == ScriptSecretsType.Host && string.Equals(keyName, MasterKeyName, StringComparison.OrdinalIgnoreCase))
111126
{
112127
operationResult = await _secretManager.SetMasterKeyAsync(value);
113128
}
114129
else
115130
{
116-
operationResult = await _secretManager.AddOrUpdateFunctionSecretAsync(keyName, value, functionName);
131+
operationResult = await _secretManager.AddOrUpdateFunctionSecretAsync(keyName, value, keyScope, secretsType);
117132
}
118133

119-
_traceWriter.VerboseFormat(Resources.TraceKeysApiSecretChange, keyName, functionName ?? "host", operationResult.Result);
134+
_traceWriter.VerboseFormat(Resources.TraceKeysApiSecretChange, keyName, keyScope ?? "host", operationResult.Result);
120135

121136
switch (operationResult.Result)
122137
{
@@ -139,22 +154,38 @@ private async Task<IHttpActionResult> AddOrUpdateFunctionSecretAsync(string keyN
139154
}
140155
}
141156

142-
private async Task<IHttpActionResult> DeleteFunctionSecretAsync(string keyName, string functionName = null)
157+
private async Task<Dictionary<string, string>> GetHostSecretsByScope(string secretsScope)
158+
{
159+
var hostSecrets = await _secretManager.GetHostSecretsAsync();
160+
161+
if (string.Equals(secretsScope, HostKeyScopes.FunctionKeys, StringComparison.OrdinalIgnoreCase))
162+
{
163+
return hostSecrets.FunctionKeys;
164+
}
165+
else if (string.Equals(secretsScope, HostKeyScopes.SystemKeys, StringComparison.OrdinalIgnoreCase))
166+
{
167+
return hostSecrets.SystemKeys;
168+
}
169+
170+
return null;
171+
}
172+
173+
private async Task<IHttpActionResult> DeleteFunctionSecretAsync(string keyName, string keyScope, ScriptSecretsType secretsType)
143174
{
144175
if (keyName == null || keyName.StartsWith("_"))
145176
{
146177
// System keys cannot be deleted.
147178
return BadRequest("Invalid key name.");
148179
}
149180

150-
if ((functionName != null && !_scriptHostManager.Instance.IsFunction(functionName)) ||
151-
!await _secretManager.DeleteSecretAsync(keyName, functionName))
181+
if ((secretsType == ScriptSecretsType.Function && !_scriptHostManager.Instance.IsFunction(keyScope)) ||
182+
!await _secretManager.DeleteSecretAsync(keyName, keyScope, secretsType))
152183
{
153184
// Either the function or the key were not found
154185
return NotFound();
155186
}
156187

157-
_traceWriter.VerboseFormat(Resources.TraceKeysApiSecretChange, keyName, functionName ?? "host", "Deleted");
188+
_traceWriter.VerboseFormat(Resources.TraceKeysApiSecretChange, keyName, keyScope ?? "host", "Deleted");
158189

159190
return StatusCode(HttpStatusCode.NoContent);
160191
}

src/WebJobs.Script.WebHost/Filters/AuthorizationLevelAttribute.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ public sealed class AuthorizationLevelAttribute : AuthorizationFilterAttribute
1818
{
1919
public const string FunctionsKeyHeaderName = "x-functions-key";
2020

21-
public AuthorizationLevelAttribute(AuthorizationLevel level)
21+
public AuthorizationLevelAttribute(AuthorizationLevel level, string keyName = null)
2222
{
2323
Level = level;
24+
KeyName = keyName;
2425
}
2526

2627
public AuthorizationLevel Level { get; }
2728

29+
public string KeyName { get; }
30+
2831
public async override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
2932
{
3033
if (actionContext == null)
@@ -36,7 +39,7 @@ public async override Task OnAuthorizationAsync(HttpActionContext actionContext,
3639
// as a request property
3740
var secretManager = actionContext.ControllerContext.Configuration.DependencyResolver.GetService<ISecretManager>();
3841
var settings = actionContext.ControllerContext.Configuration.DependencyResolver.GetService<WebHostSettings>();
39-
var requestAuthorizationLevel = await GetAuthorizationLevelAsync(actionContext.Request, secretManager);
42+
var requestAuthorizationLevel = await GetAuthorizationLevelAsync(actionContext.Request, secretManager, keyName: KeyName);
4043
actionContext.Request.Properties[ScriptConstants.AzureFunctionsHttpRequestAuthorizationLevel] = requestAuthorizationLevel;
4144

4245
if (settings.IsAuthDisabled ||
@@ -52,7 +55,7 @@ public async override Task OnAuthorizationAsync(HttpActionContext actionContext,
5255
}
5356
}
5457

55-
internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRequestMessage request, ISecretManager secretManager, string functionName = null)
58+
internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRequestMessage request, ISecretManager secretManager, string functionName = null, string keyName = null)
5659
{
5760
// first see if a key value is specified via headers or query string (header takes precedence)
5861
IEnumerable<string> values;
@@ -77,9 +80,13 @@ internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRe
7780
return AuthorizationLevel.Admin;
7881
}
7982

83+
if (HasMatchingKey(hostSecrets.SystemKeys, keyValue, keyName))
84+
{
85+
return AuthorizationLevel.System;
86+
}
87+
8088
// see if the key specified matches the host function key
81-
if (hostSecrets.FunctionKeys != null &&
82-
hostSecrets.FunctionKeys.Any(k => Key.SecretValueEquals(keyValue, k.Value)))
89+
if (HasMatchingKey(hostSecrets.FunctionKeys, keyValue, keyName))
8390
{
8491
return AuthorizationLevel.Function;
8592
}
@@ -88,8 +95,7 @@ internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRe
8895
if (functionName != null)
8996
{
9097
IDictionary<string, string> functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName);
91-
if (functionSecrets != null &&
92-
functionSecrets.Values.Any(s => Key.SecretValueEquals(keyValue, s)))
98+
if (HasMatchingKey(functionSecrets, keyValue, keyName))
9399
{
94100
return AuthorizationLevel.Function;
95101
}
@@ -99,6 +105,10 @@ internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRe
99105
return AuthorizationLevel.Anonymous;
100106
}
101107

108+
private static bool HasMatchingKey(IDictionary<string, string> secrets, string keyValue, string keyName)
109+
=> secrets != null &&
110+
secrets.Any(kvp => (keyName == null || string.Equals(kvp.Key, keyName, StringComparison.OrdinalIgnoreCase)) && Key.SecretValueEquals(kvp.Value, keyValue));
111+
102112
internal static bool SkipAuthorization(HttpActionContext actionContext)
103113
{
104114
return actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0

src/WebJobs.Script.WebHost/GlobalSuppressions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,6 @@
8989
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.MetricsEventManager.#BeginEvent(System.String,System.String)")]
9090
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemMetricEvent.#DebugValue")]
9191
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.Controllers.AdminController.#Ping()")]
92+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.HostSecretsInfo.#SystemKeys")]
93+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.FunctionSecrets.#Keys")]
94+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.HostSecrets.#SystemKeys")]

src/WebJobs.Script.WebHost/Security/FunctionSecrets.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ public FunctionSecrets(IList<Key> keys)
2626
[JsonIgnore]
2727
public override bool HasStaleKeys => Keys?.Any(k => k.IsStale) ?? false;
2828

29-
[JsonIgnore]
30-
protected override ICollection<Key> InnerFunctionKeys => Keys;
31-
3229
[JsonIgnore]
3330
public override ScriptSecretsType SecretsType => ScriptSecretsType.Function;
3431

32+
protected override ICollection<Key> GetKeys(string keyScope) => Keys;
33+
3534
public override ScriptSecrets Refresh(IKeyValueConverterFactory factory)
3635
{
3736
var keys = Keys.Select(k => factory.GetValueWriter(k).WriteValue(k)).ToList();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. 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.Web;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.WebHost
10+
{
11+
public static class HostKeyScopes
12+
{
13+
public const string FunctionKeys = "functionkeys";
14+
15+
public const string SystemKeys = "systemkeys";
16+
}
17+
}

src/WebJobs.Script.WebHost/Security/HostSecrets.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,37 @@ public class HostSecrets : ScriptSecrets
1515
[JsonProperty(PropertyName = "functionKeys")]
1616
public IList<Key> FunctionKeys { get; set; }
1717

18-
[JsonIgnore]
19-
public override bool HasStaleKeys => (MasterKey?.IsStale ?? false) || (FunctionKeys?.Any(k => k.IsStale) ?? false);
18+
[JsonProperty(PropertyName = "systemKeys")]
19+
public IList<Key> SystemKeys { get; set; }
2020

2121
[JsonIgnore]
22-
protected override ICollection<Key> InnerFunctionKeys => FunctionKeys;
22+
public override bool HasStaleKeys => (MasterKey?.IsStale ?? false)
23+
|| (FunctionKeys?.Any(k => k.IsStale) ?? false) || (SystemKeys?.Any(k => k.IsStale) ?? false);
2324

2425
[JsonIgnore]
2526
public override ScriptSecretsType SecretsType => ScriptSecretsType.Host;
2627

28+
protected override ICollection<Key> GetKeys(string keyScope)
29+
{
30+
if (string.Equals(keyScope, HostKeyScopes.FunctionKeys, System.StringComparison.OrdinalIgnoreCase))
31+
{
32+
return FunctionKeys;
33+
}
34+
else if (string.Equals(keyScope, HostKeyScopes.SystemKeys, System.StringComparison.OrdinalIgnoreCase))
35+
{
36+
return SystemKeys;
37+
}
38+
39+
return null;
40+
}
41+
2742
public override ScriptSecrets Refresh(IKeyValueConverterFactory factory)
2843
{
2944
var secrets = new HostSecrets
3045
{
3146
MasterKey = factory.WriteKey(MasterKey),
32-
FunctionKeys = FunctionKeys.Select(k => factory.WriteKey(k)).ToList()
47+
FunctionKeys = FunctionKeys.Select(k => factory.WriteKey(k)).ToList(),
48+
SystemKeys = SystemKeys.Select(k => factory.WriteKey(k)).ToList()
3349
};
3450

3551
return secrets;

src/WebJobs.Script.WebHost/Security/HostSecretsInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ public class HostSecretsInfo
1010
public string MasterKey { get; set; }
1111

1212
public Dictionary<string, string> FunctionKeys { get; set; }
13+
14+
public Dictionary<string, string> SystemKeys { get; set; }
1315
}
1416
}

src/WebJobs.Script.WebHost/Security/ISecretManager.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ public interface ISecretManager
1313
/// Deletes a function secret.
1414
/// </summary>
1515
/// <param name="secretName">The name of the secret to be deleted.</param>
16-
/// <param name="functionName">The function name, in case of a function level secret; <see cref="null"/> if this is a host level function secret.</param>
16+
/// <param name="keyScope">The target scope for the key. In case of a function level secrets, this will be the name of the function,
17+
/// for host level secrets, this will identify the host secret type.</param>
18+
/// <param name="secretsType">The target secrets type.</param>
1719
/// <returns>True if the secret was successfully deleted; otherwise, false.</returns>
18-
Task<bool> DeleteSecretAsync(string secretName, string functionName = null);
20+
Task<bool> DeleteSecretAsync(string secretName, string keyScope, ScriptSecretsType secretsType);
1921

2022
/// <summary>
2123
/// Retrieves function secrets.
@@ -37,9 +39,11 @@ public interface ISecretManager
3739
/// </summary>
3840
/// <param name="secretName">The name of the secret to be created or updated.</param>
3941
/// <param name="secret">The secret value.</param>
40-
/// <param name="functionName">The optional function name. If not provided, the function secret will be created at the host level.</param>
42+
/// <param name="keyScope">The target scope for the key. For function level secrets, this will be the name of the function,
43+
/// for host level secrets, this will identify the host secret type.</param>
44+
/// <param name="secretsType">The target secrets type.</param>
4145
/// <returns>A <see cref="Task"/> that completes when the operation is finished.</returns>
42-
Task<KeyOperationResult> AddOrUpdateFunctionSecretAsync(string secretName, string secret, string functionName = null);
46+
Task<KeyOperationResult> AddOrUpdateFunctionSecretAsync(string secretName, string secret, string keyScope, ScriptSecretsType secretsType);
4347

4448
/// <summary>
4549
/// Updates the host master key.

0 commit comments

Comments
 (0)