This commit is contained in:
sushilraje 2018-11-14 22:00:27 -08:00
Родитель bdd065d4fc 862fc412ba
Коммит b5a7ac4f3d
140 изменённых файлов: 3480 добавлений и 140 удалений

Просмотреть файл

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.AsaManager.Services.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Services.Test.helpers;
using Xunit;
namespace Services.Test
{
public class ActionConverterTest
{
private const string PARAM_NOTES = "Chiller pressure is at 250 which is high";
private const string PARAM_SUBJECT = "Alert Notification";
private const string PARAM_RECIPIENTS = "sampleEmail@gmail.com";
private const string PARAM_NOTES_KEY = "Notes";
private const string PARAM_RECIPIENTS_KEY = "Recipients";
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ItReturnsEmailAction_WhenEmailActionJsonPassed()
{
// Arrange
const string SAMPLE_JSON = "[{\"Type\":\"Email\"," +
"\"Parameters\":{\"Notes\":\"" + PARAM_NOTES +
"\",\"Subject\":\"" + PARAM_SUBJECT +
"\",\"Recipients\":[\"" + PARAM_RECIPIENTS + "\"]}}]";
// Act
var rulesList = JsonConvert.DeserializeObject<List<IActionApiModel>>(SAMPLE_JSON);
// Assert
Assert.NotEmpty(rulesList);
Assert.Equal(ActionType.Email, rulesList[0].Type);
Assert.Equal(PARAM_NOTES, rulesList[0].Parameters[PARAM_NOTES_KEY]);
Assert.Equal(new JArray { PARAM_RECIPIENTS }, rulesList[0].Parameters[PARAM_RECIPIENTS_KEY]);
}
}
}

Просмотреть файл

@ -0,0 +1,185 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.AsaManager.Services.Models;
using Newtonsoft.Json;
using Services.Test.helpers;
using Xunit;
namespace Services.Test.Models
{
public class EmailActionApiModelTest
{
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void EmptyActionsAreEqual()
{
// Arrange
var action = new EmailActionApiModel();
var action2 = new EmailActionApiModel();
// Assert
Assert.Equal(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsAreEqual_WithNoParameters()
{
// Arrange: action without parameters
var action = new EmailActionApiModel()
{
Type = ActionType.Email
};
var action2 = Clone(action);
// Assert
Assert.Equal(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsAreEqual_WithParameters()
{
// Arrange: action with parameters
var action = new EmailActionApiModel()
{
Type = ActionType.Email,
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
// Assert
Assert.Equal(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsWithDifferentDataAreDifferent()
{
// Arrange
var action = new EmailActionApiModel()
{
Type = ActionType.Email,
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
var action3 = Clone(action);
action2.Parameters.Add("key1", "x");
action3.Parameters["Notes"] += "sample string";
// Assert
Assert.NotEqual(action, action2);
Assert.NotEqual(action, action3);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsWithDifferentKeysAreDifferent()
{
// Arrange: different number of key-value pairs in Parameters.
var action = new EmailActionApiModel()
{
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
action2.Parameters.Add("Key1", "Value1");
// Assert
Assert.NotEqual(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsWithDifferentKayValuesAreDifferent()
{
// Arrange: different template
var action = new EmailActionApiModel()
{
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
action2.Parameters["Notes"] = "Changing note";
// Assert
Assert.NotEqual(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsWithDifferentRecipientsAreDifferent()
{
//Arrange: Differet list of email
var action = new EmailActionApiModel
{
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
action2.Parameters["Recipients"] = new List<string> { "sampleEmail1@gmail.com", "sampleEmail2@gmail.com", "samleEmail3@gmail.com" };
// Assert
Assert.NotEqual(action, action2);
// Arrange: Different list of email, same length
action2.Parameters["Recipients"] = new List<string>() { "anotherEmail1@gmail.com", "anotherEmail2@gmail.com" };
// Assert
Assert.NotEqual(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionsWithSameRecipientsAreSame()
{
// Arrange: Same list of email different order.
var action = new EmailActionApiModel()
{
Parameters = this.CreateSampleParameters()
};
var action2 = Clone(action);
action2.Parameters["Recipients"] = new List<string>() { "sampleEmail2@gmail.com", "sampleEmail1@gmail.com" };
// Assert
Assert.Equal(action, action2);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ActionComparisonIsCaseInsensitive()
{
// Arrange: Same list of email different order.
var actionDict = new Dictionary<string, object>()
{
{ "Type", "Email" },
{ "Parameters", this.CreateSampleParameters() }
};
var actionDict2 = new Dictionary<string, object>()
{
{ "Type", "Email" },
{ "Parameters", new Dictionary<string, object>()
{
{ "noTeS", "Sample Note" },
{ "REcipienTs", new List<string>() {"sampleEmail2@gmail.com", "sampleEmail1@gmail.com"} }
} }
};
var jsonAction = JsonConvert.SerializeObject(actionDict);
var jsonAction2 = JsonConvert.SerializeObject(actionDict2);
var action = JsonConvert.DeserializeObject<EmailActionApiModel>(jsonAction);
var action2 = JsonConvert.DeserializeObject<EmailActionApiModel>(jsonAction2);
Assert.Equal(action, action2);
}
private static T Clone<T>(T o)
{
var a = JsonConvert.SerializeObject(o);
return JsonConvert.DeserializeObject<T>(
JsonConvert.SerializeObject(o));
}
private Dictionary<string, object> CreateSampleParameters()
{
return new Dictionary<string, object>()
{
{ "Notes", "Sample Note" },
{ "Recipients", new List<string> { "sampleEmail1@gmail.com", "sampleEmail2@gmail.com" } }
};
}
}
}

Просмотреть файл

@ -61,6 +61,24 @@ namespace Services.Test.Models
// Assert
Assert.True(x.Equals(y));
// Arrange: rule with actions
x.Actions = new List<IActionApiModel>
{
new EmailActionApiModel
{
Type = ActionType.Email,
Parameters = new Dictionary<string, object>
{
{ "Notes", "Sample Note" },
{ "Recipients", new List<string> { "sampleEmail1@gmail.com", "sampleEmail2@gmail.com" } }
}
}
};
y = Clone(x);
// Assert
Assert.True(x.Equals(y));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Azure.IoTSolutions.AsaManager.Services.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.JsonConverters
{
public class ActionConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
private const string TYPE_KEY = "Type";
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IActionApiModel);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var action = default(EmailActionApiModel);
var actionType = Enum.Parse(
typeof(ActionType),
jsonObject.GetValue(TYPE_KEY).Value<string>(),
true);
switch (actionType)
{
case ActionType.Email:
action = new EmailActionApiModel();
break;
}
serializer.Populate(jsonObject.CreateReader(), action);
return action;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Use default implementation for writing to the field.");
}
}
}

Просмотреть файл

@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.JsonConverters
{
public class EmailParametersDictionaryConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
private const string RECIPIENTS_KEY = "Recipients";
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Dictionary<string, object>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jsonObject = JObject.Load(reader);
// Convert to a case-insensitive dictionary for case insensitive look up.
Dictionary<string, object> returnDictionary =
new Dictionary<string, object>(jsonObject.ToObject<Dictionary<string, object>>(), StringComparer.OrdinalIgnoreCase);
if (returnDictionary.ContainsKey(RECIPIENTS_KEY) && returnDictionary[RECIPIENTS_KEY] != null)
{
returnDictionary[RECIPIENTS_KEY] = ((JArray)returnDictionary[RECIPIENTS_KEY]).ToObject<List<string>>();
}
return returnDictionary;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Use default implementation for writing to the field.");
}
}
}

Просмотреть файл

@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.IoTSolutions.AsaManager.Services.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
{
public class EmailActionApiModel : IActionApiModel
{
[JsonConverter(typeof(StringEnumConverter))]
public ActionType Type { get; set; }
// Parameters dictionary is case-insensitive.
[JsonConverter(typeof(EmailParametersDictionaryConverter))]
public IDictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
public override bool Equals(object obj)
{
if (!(obj is EmailActionApiModel otherApiModel))
{
return false;
}
return this.Type.Equals(otherApiModel.Type)
&& this.IsEqualDictionary(otherApiModel.Parameters);
}
public override int GetHashCode()
{
var hashCode = this.Type.GetHashCode();
hashCode = (hashCode * 397) ^ (this.Parameters != null ? this.Parameters.GetHashCode() : 0);
return hashCode;
}
// Checks if both the dictionaries have the same keys and values.
// For a dictionary[key] => list, does a comparison of all the elements of the list, regardless of order.
private bool IsEqualDictionary(IDictionary<string, object> compareDictionary)
{
if (this.Parameters.Count != compareDictionary.Count) return false;
foreach (var key in this.Parameters.Keys)
{
if (!compareDictionary.ContainsKey(key) ||
this.Parameters[key].GetType() != compareDictionary[key].GetType())
{
return false;
}
if (this.Parameters[key] is IList<string> &&
!this.AreListsEqual((List<string>)this.Parameters[key], (List<string>)compareDictionary[key]))
{
return false;
}
if (!(this.Parameters[key] is IList<string>) &&
!compareDictionary[key].Equals(this.Parameters[key]))
{
return false;
}
}
return true;
}
private bool AreListsEqual(List<string> list1, List<string> list2)
{
return list1.Count == list2.Count && !list1.Except(list2).Any();
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.AsaManager.Services.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
{
[JsonConverter(typeof(ActionConverter))]
public interface IActionApiModel
{
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("Type")]
ActionType Type { get; set; }
// Dictionary should always be initialized as a case-insensitive dictionary
[JsonProperty("Parameters")]
IDictionary<string, object> Parameters { get; set; }
}
public enum ActionType
{
Email
}
}

Просмотреть файл

@ -12,6 +12,7 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
public RuleApiModel()
{
this.Conditions = new List<ConditionApiModel>();
this.Actions = new List<IActionApiModel>();
}
[JsonProperty("Id")]
@ -41,6 +42,9 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
[JsonProperty("TimePeriod")]
public long TimePeriod { get; set; }
[JsonProperty(PropertyName = "Actions")]
public List<IActionApiModel> Actions { get; set; }
[JsonProperty("Deleted")]
public bool Deleted { get; set; }
@ -48,18 +52,29 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
{
if (!(obj is RuleApiModel x)) return false;
var count = this.Conditions.Count();
var conditionsMatch = x.Conditions.Count() == count;
while (conditionsMatch && --count >= 0)
if (this.Conditions.Count != x.Conditions.Count
|| this.Actions.Count != x.Actions.Count)
{
conditionsMatch = conditionsMatch
&& this.Conditions[count].Equals(x.Conditions[count]);
return false;
}
// Compare everything
return conditionsMatch
&& string.Equals(this.Id, x.Id)
if (this.Conditions.Except(x.Conditions).Any())
{
return false;
}
for (int i = 0; i < this.Actions.Count; i++)
{
{
if (!this.Actions[i].Equals(x.Actions[i]))
{
return false;
}
}
}
// Compare all other parameters if conditions and actions are equal
return string.Equals(this.Id, x.Id)
&& string.Equals(this.Name, x.Name)
&& string.Equals(this.Description, x.Description)
&& this.Enabled == x.Enabled
@ -78,9 +93,11 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.Services.Models
hashCode = (hashCode * 397) ^ (this.Name != null ? this.Name.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (this.Description != null ? this.Description.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ this.Enabled.GetHashCode();
hashCode = (hashCode * 397) ^ this.Deleted.GetHashCode();
hashCode = (hashCode * 397) ^ (this.GroupId != null ? this.GroupId.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (this.Severity != null ? this.Severity.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (this.Conditions != null ? this.Conditions.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (this.Actions != null ? this.Actions.GetHashCode() : 0);
return hashCode;
}
}

Просмотреть файл

@ -76,13 +76,14 @@ namespace TelemetryRulesAgent.Test.Models
// Assert
var expectedJSON = JsonConvert.SerializeObject(new
{
Id = (string) null,
Name = (string) null,
Description = (string) null,
GroupId = (string) null,
Severity = (string) null,
AggregationWindow = (string) null,
Id = (string)null,
Name = (string)null,
Description = (string)null,
GroupId = (string)null,
Severity = (string)null,
AggregationWindow = (string)null,
Fields = new string[] { },
Actions = new List<object>(),
__rulefilterjs = "return true;"
});
Assert.Equal(expectedJSON, json);
@ -100,6 +101,7 @@ namespace TelemetryRulesAgent.Test.Models
Description = Guid.NewGuid().ToString(),
GroupId = Guid.NewGuid().ToString(),
Severity = Guid.NewGuid().ToString(),
Actions = new List<IActionApiModel>() { GetSampleActionData() },
Calculation = SOURCE_NO_AGGREGATION
};
@ -118,6 +120,7 @@ namespace TelemetryRulesAgent.Test.Models
Severity = rule.Severity,
AggregationWindow = ASA_AGGREGATION_NONE,
Fields = new string[] { },
Actions = rule.Actions,
__rulefilterjs = "return true;"
});
Assert.Equal(expectedJSON, json);
@ -141,6 +144,7 @@ namespace TelemetryRulesAgent.Test.Models
Description = Guid.NewGuid().ToString(),
GroupId = Guid.NewGuid().ToString(),
Severity = Guid.NewGuid().ToString(),
Actions = new List<IActionApiModel>() { GetSampleActionData() },
Calculation = SOURCE_AVG_AGGREGATOR,
TimePeriod = sourceAggr
};
@ -160,6 +164,7 @@ namespace TelemetryRulesAgent.Test.Models
Severity = rule.Severity,
AggregationWindow = asaAggr,
Fields = new string[] { },
Actions = rule.Actions,
__rulefilterjs = "return true;"
});
Assert.Equal(expectedJSON, json);
@ -193,6 +198,7 @@ namespace TelemetryRulesAgent.Test.Models
Description = Guid.NewGuid().ToString(),
GroupId = Guid.NewGuid().ToString(),
Severity = Guid.NewGuid().ToString(),
Actions = new List<IActionApiModel>() { GetSampleActionData() },
Calculation = SOURCE_NO_AGGREGATION,
Conditions = new List<ConditionApiModel>
{
@ -235,6 +241,7 @@ namespace TelemetryRulesAgent.Test.Models
Severity = rule.Severity,
AggregationWindow = ASA_AGGREGATION_NONE,
Fields = rule.Conditions.Select(x => x.Field),
Actions = rule.Actions,
__rulefilterjs = $"return ({cond1} && {cond2} && {cond3}) ? true : false;"
});
Assert.Equal(expectedJSON, json);
@ -268,6 +275,7 @@ namespace TelemetryRulesAgent.Test.Models
Description = Guid.NewGuid().ToString(),
GroupId = Guid.NewGuid().ToString(),
Severity = Guid.NewGuid().ToString(),
Actions = new List<IActionApiModel>() { GetSampleActionData() },
Calculation = SOURCE_AVG_AGGREGATOR,
TimePeriod = SOURCE_5MINS_AGGREGATION,
Conditions = new List<ConditionApiModel>
@ -311,6 +319,7 @@ namespace TelemetryRulesAgent.Test.Models
Severity = rule.Severity,
AggregationWindow = ASA_AGGREGATION_WINDOW_TUMBLING_5MINS,
Fields = rule.Conditions.Select(x => x.Field),
Actions = rule.Actions,
__rulefilterjs = $"return ({cond1} && {cond2} && {cond3}) ? true : false;"
});
Assert.Equal(expectedJSON, json);
@ -332,6 +341,7 @@ namespace TelemetryRulesAgent.Test.Models
Description = Guid.NewGuid().ToString(),
GroupId = Guid.NewGuid().ToString(),
Severity = Guid.NewGuid().ToString(),
Actions = new List<IActionApiModel>() { GetSampleActionData() },
Calculation = aggregator,
TimePeriod = SOURCE_5MINS_AGGREGATION,
Conditions = new List<ConditionApiModel>
@ -356,9 +366,23 @@ namespace TelemetryRulesAgent.Test.Models
Severity = rule.Severity,
AggregationWindow = ASA_AGGREGATION_WINDOW_TUMBLING_5MINS,
Fields = rule.Conditions.Select(x => x.Field),
Actions = rule.Actions,
__rulefilterjs = $"return ({cond1}) ? true : false;"
});
Assert.Equal(expectedJSON, json);
}
public static EmailActionApiModel GetSampleActionData()
{
return new EmailActionApiModel()
{
Type = ActionType.Email,
Parameters = new Dictionary<string, object>()
{
{ "Notes", "This is a new email" },
{ "Recipients", new List<string>(){"azureTest2@gmail.com", "azureTest@gmail.com"} }
}
};
}
}
}

Просмотреть файл

@ -114,6 +114,9 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.TelemetryRulesAgent.Models
[JsonProperty("Fields")]
public List<string> Fields { get; set; }
[JsonProperty("Actions", NullValueHandling = NullValueHandling.Ignore)]
public List<IActionApiModel> Actions { get; set; }
[JsonProperty("__rulefilterjs")]
public string RuleFilterJs => this.ConditionsToJavascript();
@ -148,6 +151,11 @@ namespace Microsoft.Azure.IoTSolutions.AsaManager.TelemetryRulesAgent.Models
this.conditions.Add(condition);
this.Fields.Add(c.Field);
}
if (rule.Actions != null && rule.Actions.Count > 0)
{
this.Actions = rule.Actions;
}
}
private static string GetAggregationWindowValue(string calculation, long timePeriod)

Просмотреть файл

@ -122,6 +122,7 @@ CombineAggregatedMeasurementsAndRules AS (
FA.__lastReceivedTime,
R.Description as __description,
R.Severity as __severity,
R.Actions as __actions,
R.__rulefilterjs as __rulefilterjs
FROM
FlatAggregatedMeasurements FA PARTITION BY PartitionId
@ -175,6 +176,7 @@ CombineInstantMeasurementsAndRules as
FI.__aggregates,
R.Description as __description,
R.Severity as __severity,
R.Actions as __actions,
R.__rulefilterjs as __rulefilterjs
FROM
FlatInstantMeasurements FI PARTITION BY PartitionId
@ -201,6 +203,7 @@ CombineAlarms as
DATEDIFF(millisecond, '1970-01-01T00:00:00Z', System.Timestamp) as modified,
AA.__description as [rule.description],
AA.__severity as [rule.severity],
AA.__actions as [rule.actions],
AA.__ruleid as [rule.id],
AA.__deviceId as [device.id],
AA.__aggregates,
@ -219,6 +222,7 @@ CombineAlarms as
DATEDIFF(millisecond, '1970-01-01T00:00:00Z', System.Timestamp) as modified,
AI.__description as [rule.description],
AI.__severity as [rule.severity],
AI.__actions as [rule.actions],
AI.__ruleid as [rule.id],
AI.__deviceId as [device.id],
AI.__aggregates,
@ -229,12 +233,39 @@ CombineAlarms as
-- Output alarm events
SELECT
CA.*
CA.[doc.schemaVersion],
CA.[doc.schema],
CA.[status],
CA.[logic],
CA.[created],
CA.[modified],
CA.[rule.description],
CA.[rule.severity],
CA.[rule.id],
CA.[device.id],
CA.[device.msg.received]
INTO
Alarms
FROM
CombineAlarms CA PARTITION BY PartitionId
-- Output action events
SELECT
CA.[created],
CA.[modified],
CA.[rule.description],
CA.[rule.severity],
CA.[rule.id],
CA.[rule.actions],
CA.[device.id],
CA.[device.msg.received]
INTO
Actions
FROM
CombineAlarms CA PARTITION BY __partitionid
WHERE
CA.[rule.actions] IS NOT NULL
-- Output origin telemetry messages
SELECT
CONCAT(T.IoTHub.ConnectionDeviceId, ';', CAST(DATEDIFF(millisecond, '1970-01-01T00:00:00Z', T.EventEnqueuedUtcTime) AS nvarchar(max))) as id,

Просмотреть файл

@ -122,6 +122,7 @@ CombineAggregatedMeasurementsAndRules AS (
FA.__lastReceivedTime,
R.Description as __description,
R.Severity as __severity,
R.Actions as __actions,
R.__rulefilterjs as __rulefilterjs
FROM
FlatAggregatedMeasurements FA PARTITION BY PartitionId
@ -175,6 +176,7 @@ CombineInstantMeasurementsAndRules as
FI.__aggregates,
R.Description as __description,
R.Severity as __severity,
R.Actions as __actions,
R.__rulefilterjs as __rulefilterjs
FROM
FlatInstantMeasurements FI PARTITION BY PartitionId
@ -201,6 +203,7 @@ CombineAlarms as
DATEDIFF(millisecond, '1970-01-01T00:00:00Z', System.Timestamp) as modified,
AA.__description as [rule.description],
AA.__severity as [rule.severity],
AA.__actions as [rule.actions],
AA.__ruleid as [rule.id],
AA.__deviceId as [device.id],
AA.__aggregates,
@ -219,6 +222,7 @@ CombineAlarms as
DATEDIFF(millisecond, '1970-01-01T00:00:00Z', System.Timestamp) as modified,
AI.__description as [rule.description],
AI.__severity as [rule.severity],
AI.__actions as [rule.actions],
AI.__ruleid as [rule.id],
AI.__deviceId as [device.id],
AI.__aggregates,
@ -229,8 +233,35 @@ CombineAlarms as
-- Output alarm events
SELECT
CA.*
CA.[doc.schemaVersion],
CA.[doc.schema],
CA.[status],
CA.[logic],
CA.[created],
CA.[modified],
CA.[rule.description],
CA.[rule.severity],
CA.[rule.id],
CA.[device.id],
CA.[device.msg.received]
INTO
Alarms
FROM
CombineAlarms CA PARTITION BY PartitionId
CombineAlarms CA PARTITION BY PartitionId
-- Output action events
SELECT
CA.[created],
CA.[modified],
CA.[rule.description],
CA.[rule.severity],
CA.[rule.id],
CA.[rule.actions],
CA.[device.id],
CA.[device.msg.received]
INTO
Actions
FROM
CombineAlarms CA PARTITION BY __partitionid
WHERE
CA.[rule.actions] IS NOT NULL

Просмотреть файл

@ -55,6 +55,10 @@ How to use the microservice
1. Define environment variables, as needed. See [Configuration and Environment variables](#configuration-and-environment-variables) for detailed information for setting these for your enviroment.
1. `PCS_AUTH_AUDIENCE` = {your AAD application ID}
1. `PCS_AUTH_ISSUER` = {your AAD issuer URL}
1. `PCS_AAD_ENDPOINT_URL` = {your AAD endpoint URL}
1. `PCS_AAD_TENANT` = {your AAD tenant Id}
1. `PCS_AAD_APPSECRET` = {your AAD application secret}
1. `PCS_ARM_ENDPOINT_URL` = {Azure Resource Manager URL}
1. Start the WebService project (e.g. press F5).
1. Use an HTTP client such as [Postman][postman-url], to exercise the
RESTful API.

Просмотреть файл

@ -5,6 +5,7 @@ using System.Linq;
using System.Security.Claims;
using Microsoft.Azure.IoTSolutions.Auth.Services;
using Microsoft.Azure.IoTSolutions.Auth.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.Auth.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.Auth.Services.Models;
using Microsoft.Azure.IoTSolutions.Auth.Services.Runtime;
using Moq;
@ -59,6 +60,9 @@ namespace Services.Test
Assert.Equal(claims.FirstOrDefault(k => k.Type == NAME_KEY).Value, result.Name);
Assert.Equal(claims.FirstOrDefault(k => k.Type == ID_KEY).Value, result.Id);
Assert.NotEmpty(result.AllowedActions);
Assert.NotEmpty(result.Roles);
Assert.Contains(ADMIN_ROLE_KEY, result.Roles);
Assert.Contains(READONLY_ROLE_KEY, result.Roles);
foreach (var action in adminPolicy.AllowedActions)
{
Assert.Contains(action, result.AllowedActions);
@ -87,6 +91,48 @@ namespace Services.Test
Assert.Empty(readonlyActions);
}
[InlineData(null, null, null, null, null, true)]
[InlineData("", null, null, null, null, true)]
[InlineData("https://login.microsoftonline.com/", null, null, null, null, true)]
[InlineData("https://login.microsoftonline.com/", "", null, null, null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", null, null, null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "", null, null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "https://management.azure.com/", null, null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "https://management.azure.com/", "", null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "https://management.azure.com/", "myAppId", null, true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "https://management.azure.com/", "myAppId", "", true)]
[InlineData("https://login.microsoftonline.com/", "tenantId", "https://management.azure.com/", "myAppId", "mysecret", true)]
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async void GetToken_ReturnValues(
string aadEndpointUrl,
string aadTenantId,
string audience,
string applicationId,
string secret,
bool exceptionThrown)
{
// Arrange
AccessToken token = null;
this.servicesConfig.SetupProperty(x => x.AadEndpointUrl, aadEndpointUrl);
this.servicesConfig.SetupProperty(x => x.AadTenantId, aadTenantId);
this.servicesConfig.SetupProperty(x => x.AadApplicationId, applicationId);
this.servicesConfig.SetupProperty(x => x.AadApplicationSecret, secret);
// Act
try
{
token = await this.users.GetToken(audience);
}
catch (InvalidConfigurationException e)
{
// Assert
Assert.True(exceptionThrown);
return;
}
Assert.NotNull(token);
}
private List<Claim> GetClaimWithUserInfo()
{
return new List<Claim>()
@ -134,7 +180,8 @@ namespace Services.Test
"UpdateRules",
"DeleteRules",
"CreateJobs",
"UpdateSimManagement",
"UpdateSIMManagement",
"AcquireToken",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",
@ -161,7 +208,8 @@ namespace Services.Test
"CreateRules",
"UpdateRules",
"CreateJobs",
"UpdateSimManagement",
"UpdateSIMManagement",
"AcquireToken",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",

Просмотреть файл

@ -0,0 +1,23 @@
using System;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace Microsoft.Azure.IoTSolutions.Auth.Services.Models
{
public class AccessToken
{
public AccessToken(string audience, AuthenticationResult authenticationResult)
{
this.Audience = audience;
this.Value = authenticationResult.AccessToken;
this.Type = authenticationResult.AccessTokenType;
this.ExpiresOn = authenticationResult.ExpiresOn;
this.Authority = authenticationResult.Authority;
}
public string Audience { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public string Authority { get; set; }
public DateTimeOffset ExpiresOn { get; set; }
}
}

Просмотреть файл

@ -10,5 +10,6 @@ namespace Microsoft.Azure.IoTSolutions.Auth.Services.Models
public string Name { get; set; }
public string Email { get; set; }
public List<string> AllowedActions { get; set; }
public List<string> Roles { get; set; }
}
}

Просмотреть файл

@ -12,6 +12,11 @@ namespace Microsoft.Azure.IoTSolutions.Auth.Services.Runtime
IEnumerable<string> JwtEmailFrom { get; set; }
string JwtRolesFrom { get; set; }
string PoliciesFolder { get; }
string AadEndpointUrl { get; set; }
string AadTenantId { get; set; }
string AadApplicationId { get; set; }
string AadApplicationSecret { get; set; }
string ArmEndpointUrl { get; }
}
public class ServicesConfig : IServicesConfig
@ -22,6 +27,11 @@ namespace Microsoft.Azure.IoTSolutions.Auth.Services.Runtime
public IEnumerable<string> JwtNameFrom { get; set; }
public IEnumerable<string> JwtEmailFrom { get; set; }
public string JwtRolesFrom { get; set; }
public string AadEndpointUrl { get; set; }
public string AadTenantId { get; set; }
public string AadApplicationId { get; set; }
public string AadApplicationSecret { get; set; }
public string ArmEndpointUrl { get; set; }
public ServicesConfig()
{

Просмотреть файл

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.19.8" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="2.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.1.4" />

Просмотреть файл

@ -4,16 +4,20 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Azure.IoTSolutions.Auth.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.Auth.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.Auth.Services.Models;
using Microsoft.Azure.IoTSolutions.Auth.Services.Runtime;
using System.Threading.Tasks;
namespace Microsoft.Azure.IoTSolutions.Auth.Services
{
public interface IUsers
{
User GetUserInfo(IEnumerable<Claim> claims);
IEnumerable<string> GetAllowedActions(IEnumerable<string> roles);
List<string> GetAllowedActions(IEnumerable<string> roles);
Task<AccessToken> GetToken(string audience);
}
public class Users : IUsers
@ -79,11 +83,12 @@ namespace Microsoft.Azure.IoTSolutions.Auth.Services
Id = id,
Name = name,
Email = email,
AllowedActions = allowedActions.ToList()
AllowedActions = allowedActions,
Roles = roles
};
}
public IEnumerable<string> GetAllowedActions(IEnumerable<string> roles)
public List<string> GetAllowedActions(IEnumerable<string> roles)
{
// ensure only unique values are added to the allowed actions list
// if duplicate actions are allowed in multiple roles
@ -96,5 +101,43 @@ namespace Microsoft.Azure.IoTSolutions.Auth.Services
return allowedActions.ToList();
}
public async Task<AccessToken> GetToken(string audience)
{
// if no audiene is provided, use Azure Resource Manager endpoint url by default
audience = string.IsNullOrEmpty(audience) ? this.config.ArmEndpointUrl : audience;
if (string.IsNullOrEmpty(this.config.AadTenantId) ||
string.IsNullOrEmpty(this.config.AadApplicationId) ||
string.IsNullOrEmpty(this.config.AadApplicationSecret))
{
var message = $"Azure Active Directory properties '{nameof(this.config.AadEndpointUrl)}', '{nameof(this.config.AadTenantId)}'" +
$", '{nameof(this.config.AadApplicationId)}' or '{nameof(this.config.AadApplicationSecret)}' are not set.";
this.log.Error(message, () => { });
throw new InvalidConfigurationException(message);
}
string authorityUrl = this.config.AadEndpointUrl.EndsWith("/") ?
$"{this.config.AadEndpointUrl}{this.config.AadTenantId}" :
$"{this.config.AadEndpointUrl}/{this.config.AadTenantId}";
var authenticationContext = new AuthenticationContext(authorityUrl, TokenCache.DefaultShared);
try
{
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(
resource: audience,
clientCredential: new ClientCredential(
clientId: this.config.AadApplicationId,
clientSecret: this.config.AadApplicationSecret));
return new AccessToken(audience, authenticationResult);
}
catch (Exception e)
{
var message = $"Unable to retrieve token with Azure Active Directory properties '{nameof(this.config.AadEndpointUrl)}', " +
$"'{nameof(this.config.AadTenantId)}', '{nameof(this.config.AadApplicationId)}' or '{nameof(this.config.AadApplicationSecret)}'.";
this.log.Error(message, () => { });
throw new InvalidConfigurationException(message, e);
}
}
}
}

Просмотреть файл

@ -17,16 +17,18 @@
"DeleteRules",
"CreateJobs",
"UpdateSIMManagement",
"AcquireToken",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",
"DeletePackages"
"DeletePackages",
"ReadAll"
]
},
{
"Id": "e5bbd0f5-128e-4362-9dd1-8f253c6082d7",
"Role": "readOnly",
"AllowedActions": []
"AllowedActions": ["ReadAll"]
}
]
}

Просмотреть файл

@ -5,6 +5,18 @@ extract_userid_from="oid"
extract_name_from="given_name,family_name"
extract_email_from="email"
extract_roles_from="roles"
policies_folder = ./data/policies/
; The AAD endpoint url to acquire ARM token for AAD application
aad_endpoint_url="https://login.microsoftonline.com/"
; The AAD Tenant Id
aad_tenant_id="00000000-0000-0000-0000-000000000000"
; The unique Id of AAD application that has already been authorized
; to access ARM resources
aad_application_id="00000000-0000-0000-0000-000000000000"
; The secret of AAD application
aad_application_secret="replaceme"
;; Azure Resource Manager endpoint url to get token
arm_endpoint_url="https://management.azure.com/"
[AuthService:ClientAuth]

Просмотреть файл

@ -7,6 +7,7 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.IoTSolutions.Auth.Services;
using Microsoft.Azure.IoTSolutions.Auth.Services.Diagnostics;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
@ -49,6 +50,7 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
private readonly RequestDelegate requestDelegate;
private readonly IConfigurationManager<OpenIdConnectConfiguration> openIdCfgMan;
private readonly IClientAuthConfig config;
private readonly IUsers usersService;
private readonly ILogger log;
private TokenValidationParameters tokenValidationParams;
private readonly bool authRequired;
@ -59,11 +61,13 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
RequestDelegate requestDelegate, // Required by ASP.NET
IConfigurationManager<OpenIdConnectConfiguration> openIdCfgMan,
IClientAuthConfig config,
IUsers usersService,
ILogger log)
{
this.requestDelegate = requestDelegate;
this.openIdCfgMan = openIdCfgMan;
this.config = config;
this.usersService = usersService;
this.log = log;
this.authRequired = config.AuthRequired;
this.tokenValidationInitialized = false;
@ -103,6 +107,9 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
var header = string.Empty;
var token = string.Empty;
// Store this setting to skip validating authorization in the controller if enabled
context.Request.SetAuthRequired(this.config.AuthRequired);
if (!context.Request.Headers.ContainsKey(EXT_RESOURCES_HEADER))
{
// This is a service to service request running in the private
@ -113,9 +120,12 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
// Call the next delegate/middleware in the pipeline
this.log.Debug("Skipping auth for service to service request", () => { });
context.Request.SetExternalRequest(false);
return this.requestDelegate(context);
}
context.Request.SetExternalRequest(true);
if (!this.authRequired)
{
// Call the next delegate/middleware in the pipeline
@ -181,6 +191,20 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
// header doesn't need to be parse again later in the User controller.
context.Request.SetCurrentUserClaims(jwtToken.Claims);
// Store the user allowed actions in the request context to validate
// authorization later in the controller.
var userObjectId = context.Request.GetCurrentUserObjectId();
var roles = context.Request.GetCurrentUserRoleClaim().ToList();
if (roles.Any())
{
var allowedActions = this.usersService.GetAllowedActions(roles);
context.Request.SetCurrentUserAllowedActions(allowedActions);
}
else
{
this.log.Error("JWT token doesn't include any role claims.", () => { });
}
return true;
}

Просмотреть файл

@ -33,6 +33,12 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
// The required audience
string JwtAudience { get; set; }
// The audience's secret
string JwtAudienceSecret { get; set; }
// Azure resource manager endpoint url
string ArmEndpointUrl { get; set; }
// Clock skew allowed when validating tokens expiration
// Default: 2 minutes
TimeSpan JwtClockSkew { get; set; }
@ -48,6 +54,8 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
public IEnumerable<string> JwtAllowedAlgos { get; set; }
public string JwtIssuer { get; set; }
public string JwtAudience { get; set; }
public string JwtAudienceSecret { get; set; }
public string ArmEndpointUrl { get; set; }
public TimeSpan JwtClockSkew { get; set; }
}
}

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
@ -8,23 +10,96 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Auth
{
public static class RequestExtension
{
private const string CONTEXT_KEY = "CurrentUserClaims";
private const string CONTEXT_KEY_USER_CLAIMS = "CurrentUserClaims";
private const string CONTEXT_KEY_AUTH_REQUIRED = "AuthRequired";
private const string CONTEXT_KEY_ALLOWED_ACTIONS = "CurrentUserAllowedActions";
private const string CONTEXT_KEY_EXTERNAL_REQUEST = "ExternalRequest";
// Role claim type
private const string ROLE_CLAIM_TYPE = "roles";
private const string USER_OBJECT_ID_CLAIM_TYPE = "oid";
// Store the current user claims in the current request
public static void SetCurrentUserClaims(this HttpRequest request, IEnumerable<Claim> claims)
{
request.HttpContext.Items[CONTEXT_KEY] = claims;
request.HttpContext.Items[CONTEXT_KEY_USER_CLAIMS] = claims;
}
// Get the user claims from the current request
public static IEnumerable<Claim> GetCurrentUserClaims(this HttpRequest request)
{
if (!request.HttpContext.Items.ContainsKey(CONTEXT_KEY))
if (!request.HttpContext.Items.ContainsKey(CONTEXT_KEY_USER_CLAIMS))
{
return new List<Claim>();
}
return request.HttpContext.Items[CONTEXT_KEY] as IEnumerable<Claim>;
return request.HttpContext.Items[CONTEXT_KEY_USER_CLAIMS] as IEnumerable<Claim>;
}
// Store authentication setting in the current request
public static void SetAuthRequired(this HttpRequest request, bool authRequired)
{
request.HttpContext.Items[CONTEXT_KEY_AUTH_REQUIRED] = authRequired;
}
// Get the authentication setting in the current request
public static bool GetAuthRequired(this HttpRequest request)
{
if (!request.HttpContext.Items.ContainsKey(CONTEXT_KEY_AUTH_REQUIRED))
{
return true;
}
return (bool)request.HttpContext.Items[CONTEXT_KEY_AUTH_REQUIRED];
}
// Store source of request in the current request
public static void SetExternalRequest(this HttpRequest request, bool external)
{
request.HttpContext.Items[CONTEXT_KEY_EXTERNAL_REQUEST] = external;
}
// Get the source of request in the current request
public static bool IsExternalRequest(this HttpRequest request)
{
if (!request.HttpContext.Items.ContainsKey(CONTEXT_KEY_EXTERNAL_REQUEST))
{
return true;
}
return (bool)request.HttpContext.Items[CONTEXT_KEY_EXTERNAL_REQUEST];
}
// Get the user's role claims from the current request
public static string GetCurrentUserObjectId(this HttpRequest request)
{
var claims = GetCurrentUserClaims(request);
return claims.Where(c => c.Type.ToLowerInvariant().Equals(USER_OBJECT_ID_CLAIM_TYPE, StringComparison.CurrentCultureIgnoreCase))
.Select(c => c.Value).First();
}
// Get the user's role claims from the current request
public static IEnumerable<string> GetCurrentUserRoleClaim(this HttpRequest request)
{
var claims = GetCurrentUserClaims(request);
return claims.Where(c => c.Type.ToLowerInvariant().Equals(ROLE_CLAIM_TYPE, StringComparison.CurrentCultureIgnoreCase))
.Select(c => c.Value);
}
// Store the current user allowed actions in the current request
public static void SetCurrentUserAllowedActions(this HttpRequest request, IEnumerable<string> allowedActions)
{
request.HttpContext.Items[CONTEXT_KEY_ALLOWED_ACTIONS] = allowedActions;
}
// Get the user's allowed actions from the current request
public static IEnumerable<string> GetCurrentUserAllowedActions(this HttpRequest request)
{
if (!request.HttpContext.Items.ContainsKey(CONTEXT_KEY_ALLOWED_ACTIONS))
{
return new List<string>();
}
return request.HttpContext.Items[CONTEXT_KEY_ALLOWED_ACTIONS] as IEnumerable<string>;
}
}
}

Просмотреть файл

@ -29,6 +29,11 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Runtime
private const string JWT_EMAIL_FROM_KEY = APPLICATION_KEY + "extract_email_from";
private const string JWT_ROLES_FROM_KEY = APPLICATION_KEY + "extract_roles_from";
private const string POLICIES_FOLDER_KEY = APPLICATION_KEY + "policies_folder";
private const string AAD_ENDPOINT_URL = APPLICATION_KEY + "aad_endpoint_url";
private const string AAD_TENANT_ID = APPLICATION_KEY + "aad_tenant_id";
private const string AAD_APPLICATION_ID = APPLICATION_KEY + "aad_application_id";
private const string AAD_APPLICATION_SECRET = APPLICATION_KEY + "aad_application_secret";
private const string ARM_ENDPOINT_URL = APPLICATION_KEY + "arm_endpoint_url";
private const string CLIENT_AUTH_KEY = APPLICATION_KEY + "ClientAuth:";
private const string CORS_WHITELIST_KEY = CLIENT_AUTH_KEY + "cors_whitelist";
@ -39,8 +44,12 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Runtime
private const string JWT_ALGOS_KEY = JWT_KEY + "allowed_algorithms";
private const string JWT_ISSUER_KEY = JWT_KEY + "issuer";
private const string JWT_AUDIENCE_KEY = JWT_KEY + "audience";
private const string JWT_CLOCK_SKEW_KEY = JWT_KEY + "clock_skew_seconds";
public const string DEFAULT_ARM_ENDPOINT_URL = "https://management.azure.com/";
public const string DEFAULT_AAD_ENDPOINT_URL = "https://login.microsoftonline.com/";
public int Port { get; }
public IServicesConfig ServicesConfig { get; }
public IClientAuthConfig ClientAuthConfig { get; }
@ -55,7 +64,12 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.Runtime
JwtNameFrom = configData.GetString(JWT_NAME_FROM_KEY, "given_name,family_name").Split(','),
JwtEmailFrom = configData.GetString(JWT_EMAIL_FROM_KEY, "email").Split(','),
JwtRolesFrom = configData.GetString(JWT_ROLES_FROM_KEY, "roles"),
PoliciesFolder = MapRelativePath(configData.GetString(POLICIES_FOLDER_KEY))
PoliciesFolder = MapRelativePath(configData.GetString(POLICIES_FOLDER_KEY)),
AadEndpointUrl = configData.GetString(AAD_ENDPOINT_URL, DEFAULT_AAD_ENDPOINT_URL),
AadTenantId = configData.GetString(AAD_TENANT_ID, String.Empty),
AadApplicationId = configData.GetString(AAD_APPLICATION_ID, String.Empty),
AadApplicationSecret = configData.GetString(AAD_APPLICATION_SECRET, String.Empty),
ArmEndpointUrl = configData.GetString(ARM_ENDPOINT_URL, DEFAULT_ARM_ENDPOINT_URL),
};
this.ClientAuthConfig = new ClientAuthConfig

Просмотреть файл

@ -8,6 +8,17 @@ extract_roles_from="roles"
# Note: when running the service with Docker, the content of the `data`
# folder can be overridden, e.g. to inject custom policies.
policies_folder = ./data/policies/
; The AAD endpoint url to acquire ARM token for AAD application
aad_endpoint_url="${?PCS_AAD_ENDPOINT_URL}"
; The AAD Tenant Id
aad_tenant_id="${?PCS_AAD_TENANT}"
; The unique Id of AAD application that has already been authorized
; to access ARM resources
aad_application_id="${?PCS_AUTH_AUDIENCE}"
; The secret of AAD application
aad_application_secret="${?PCS_AAD_APPSECRET}"
;; Azure Resource Manager endpoint url to get token
arm_endpoint_url="${?PCS_ARM_ENDPOINT_URL}"
[AuthService:ClientAuth]
@ -41,6 +52,7 @@ issuer="${?PCS_AUTH_ISSUER}"
; Also referenced as "Application Id" and "Resource Id"
; example: audience="2814e709-6a0e-4861-9594-d3b6e2b81331"
audience="${?PCS_AUTH_AUDIENCE}"
;; secret of the application audience
; When validating the token expiration, allows some clock skew
; Default: 2 minutes
clock_skew_seconds = 300

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.Auth.Services;
using Microsoft.Azure.IoTSolutions.Auth.WebService.Auth;
@ -41,5 +42,20 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Controllers
{
return this.users.GetAllowedActions(roles);
}
/// <summary>
/// This action is used by Web UI and other services to get ARM token for
/// the application to perform resource management task.
/// </summary>
/// <param name="id">user object id</param>
/// <param name="audience">audience of the token, use ARM as default audience</param>
/// <returns>token for the audience</returns>
[HttpGet("{id}/token")]
[Authorize("AcquireToken")]
public async Task<TokenApiModel> GetToken([FromRoute]string id, [FromQuery]string audience)
{
var token = await this.users.GetToken(audience);
return new TokenApiModel(token);
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Exceptions
{
/// <summary>
/// This exception is thrown when the user is not authorized to perform the action.
/// </summary>
public class NotAuthorizedException : Exception
{
public NotAuthorizedException() : base()
{
}
public NotAuthorizedException(string message) : base(message)
{
}
public NotAuthorizedException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Exceptions;
using Microsoft.Azure.IoTSolutions.Auth.WebService.Auth;
namespace Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Filters
{
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute(string allowedActions)
: base(typeof(AuthorizeActionFilterAttribute))
{
this.Arguments = new object[] { allowedActions };
}
}
public class AuthorizeActionFilterAttribute : Attribute, IAsyncActionFilter
{
private readonly string allowedAction;
public AuthorizeActionFilterAttribute(string allowedAction)
{
this.allowedAction = allowedAction;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
bool isAuthorized = this.IsValidAuthorization(context.HttpContext, this.allowedAction);
if (!isAuthorized)
{
throw new NotAuthorizedException($"Current user is not authorized to perform this action: '{this.allowedAction}'");
}
else
{
await next();
}
}
/// <summary>
/// Validate allowed actions of current user based on role claims against the declared actions
/// in the Authorize attribute of controller. The allowed action is case insensitive.
/// </summary>
/// <param name="httpContext">current context of http request</param>
/// <param name="allowedAction">allowed action required by controller</param>
/// <returns>true if validatation succeed</returns>
private bool IsValidAuthorization(HttpContext httpContext, string allowedAction)
{
if (!httpContext.Request.GetAuthRequired() || !httpContext.Request.IsExternalRequest()) return true;
if (allowedAction == null || !allowedAction.Any()) return true;
var userAllowedActions = httpContext.Request.GetCurrentUserAllowedActions();
if (userAllowedActions == null || !userAllowedActions.Any())
{
return false;
}
// validation succeeds if any required action occurs in the current user's allowed allowedAction
return userAllowedActions.Select(a => a.ToLowerInvariant())
.Contains(this.allowedAction.ToLowerInvariant());
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Azure.IoTSolutions.Auth.Services.Models;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Models
{
public class TokenApiModel
{
[JsonProperty(PropertyName = "Audience", Order = 10)]
public string Audience { get; set; }
[JsonProperty(PropertyName = "AccessTokenType", Order = 20)]
public string AccessTokenType { get; set; }
[JsonProperty(PropertyName = "AccessToken", Order = 30)]
public string AccessToken { get; set; }
[JsonProperty(PropertyName = "Authority", Order = 40)]
public string Authority { get; set; }
[JsonProperty(PropertyName = "ExpiresOn", Order = 50)]
public DateTimeOffset ExpiresOn { get; set; }
public TokenApiModel(AccessToken token)
{
this.Audience = token.Audience;
this.AccessTokenType = token.Type;
this.AccessToken = token.Value;
this.Authority = token.Authority;
this.ExpiresOn = token.ExpiresOn;
}
}
}

Просмотреть файл

@ -20,12 +20,16 @@ namespace Microsoft.Azure.IoTSolutions.Auth.WebService.v1.Models
[JsonProperty(PropertyName = "AllowedActions", Order = 40)]
public List<string> AllowedActions { get; set; }
[JsonProperty(PropertyName = "Roles", Order = 50)]
public List<string> Roles { get; set; }
public UserApiModel(User user)
{
this.Id = user.Id;
this.Email = user.Email;
this.Name = user.Name;
this.AllowedActions = user.AllowedActions;
this.Roles = user.Roles;
}
}
}

Просмотреть файл

@ -26,7 +26,8 @@ Role Policy Example:
"UpdateRules",
"DeleteRules",
"CreateJobs",
"UpdateSimManagement",
"UpdateSIMManagement",
"AcquireToken",
"CreateDeployment",
"DeleteDeployment",
"CreatePackage",

Просмотреть файл

@ -12,3 +12,5 @@ implementation, we use [Azure AAD tokens](https://docs.microsoft.com/azure/activ
The claims are added to the AAD application manifest so that the token contains the desired information.
You can learn more about optional token claims [here](https://docs.microsoft.com/azure/active-directory/active-directory-claims-mapping).
We can also get access token for resources by using application's credential so that Azure resource management task can be performed. The current user who has the role which includes 'AcquireToken' will be able to get this token.

Просмотреть файл

@ -67,6 +67,10 @@ build_in_sandbox() {
docker run -it \
-e PCS_AUTH_ISSUER \
-e PCS_AUTH_AUDIENCE \
-e PCS_AAD_ENDPOINT_URL \
-e PCS_AAD_TENANT \
-e PCS_AAD_APPSECRET \
-e PCS_ARM_ENDPOINT_URL \
-v "$PCS_CACHE/sandbox/.config:/root/.config" \
-v "$PCS_CACHE/sandbox/.dotnet:/root/.dotnet" \
-v "$PCS_CACHE/sandbox/.nuget:/root/.nuget" \

Просмотреть файл

@ -62,6 +62,10 @@ IF "%1"=="--in-sandbox" GOTO :RunInSandbox
docker run -it ^
-e PCS_AUTH_ISSUER=%PCS_AUTH_ISSUER% ^
-e PCS_AUTH_AUDIENCE=%PCS_AUTH_AUDIENCE% ^
-e PCS_AAD_ENDPOINT_URL=%PCS_AAD_ENDPOINT_URL% ^
-e PCS_AAD_TENANT=%PCS_AAD_TENANT% ^
-e PCS_AAD_APPSECRET=%PCS_AAD_APPSECRET% ^
-e PCS_ARM_ENDPOINT_URL=%PCS_ARM_ENDPOINT_URL% ^
-v %PCS_CACHE%\sandbox\.config:/root/.config ^
-v %PCS_CACHE%\sandbox\.dotnet:/root/.dotnet ^
-v %PCS_CACHE%\sandbox\.nuget:/root/.nuget ^

Просмотреть файл

@ -17,6 +17,10 @@ run_container() {
docker run -it -p 9001:9001 \
-e PCS_AUTH_ISSUER \
-e PCS_AUTH_AUDIENCE \
-e PCS_AAD_ENDPOINT_URL \
-e PCS_AAD_TENANT \
-e PCS_AAD_APPSECRET \
-e PCS_ARM_ENDPOINT_URL \
"$DOCKER_IMAGE:testing"
}

Просмотреть файл

@ -21,6 +21,10 @@ echo Starting Auth ...
docker run -it -p 9001:9001 ^
-e PCS_AUTH_ISSUER ^
-e PCS_AUTH_AUDIENCE ^
-e PCS_AAD_ENDPOINT_URL ^
-e PCS_AAD_TENANT ^
-e PCS_AAD_APPSECRET ^
-e PCS_ARM_ENDPOINT_URL ^
%DOCKER_IMAGE%:testing
:: - - - - - - - - - - - - - -

Просмотреть файл

@ -31,4 +31,24 @@ fi
if [[ -z "$PCS_AUTH_AUDIENCE" ]]; then
echo "Error: the PCS_AUTH_AUDIENCE environment variable is not defined."
exit -1
fi
if [[ -z "PCS_AAD_ENDPOINT_URL" ]]; then
echo "Error: the PCS_AAD_ENDPOINT_URL environment variable is not defined."
exit -1
fi
if [[ -z "PCS_AAD_TENANT" ]]; then
echo "Error: the PCS_AAD_TENANT environment variable is not defined."
exit -1
fi
if [[ -z "PCS_AAD_APPSECRET" ]]; then
echo "Error: the PCS_AAD_APPSECRET environment variable is not defined."
exit -1
fi
if [[ -z "PCS_ARM_ENDPOINT_URL" ]]; then
echo "Error: the PCS_ARM_ENDPOINT_URL environment variable is not defined."
exit -1
fi

Просмотреть файл

@ -10,4 +10,24 @@ IF "%PCS_AUTH_AUDIENCE%" == "" (
exit /B 1
)
IF "%PCS_AAD_ENDPOINT_URL%" == "" (
echo Error: the PCS_AAD_ENDPOINT_URL environment variable is not defined.
exit /B 1
)
IF "%PCS_AAD_TENANT%" == "" (
echo Error: the PCS_AAD_TENANT environment variable is not defined.
exit /B 1
)
IF "%PCS_AAD_APPSECRET%" == "" (
echo Error: the PCS_AAD_APPSECRET environment variable is not defined.
exit /B 1
)
IF "%PCS_ARM_ENDPOINT_URL%" == "" (
echo Error: the PCS_ARM_ENDPOINT_URL environment variable is not defined.
exit /B 1
)
endlocal

Просмотреть файл

@ -14,3 +14,15 @@ export PCS_AUTH_ISSUER="{enter the token issuer URL here}"
# The intended audience of the tokens, e.g. your Client Id
export PCS_AUTH_AUDIENCE="{enter the tokens audience here}"
# Azure Active Directory endpoint url, e.g. https://login.microsoftonline.com/
export PCS_AAD_ENDPOINT_URL="{enter the AAD endpoint URL here}"
# The tenant id of Azure Active Directory
export PCS_AAD_TENANT="{enter the tenant id of AAD here}"
# The secret of intended application audience
export PCS_AAD_APPSECRET="{enter the secret of AAD application here}"
# Azure Resource Manager endpoint url, e.g. https://management.azure.com/
export PCS_ARM_ENDPOINT_URL="{enter the endpoint URL of Azure Resource Manager here}"

Просмотреть файл

@ -5,3 +5,15 @@ SETX PCS_AUTH_ISSUER "{enter the token issuer URL here}"
:: The intended audience of the tokens, e.g. your Client Id
SETX PCS_AUTH_AUDIENCE "{enter the tokens audience here}"
# Azure Active Directory endpoint url, e.g. https://login.microsoftonline.com/
SETX PCS_AAD_ENDPOINT_URL "{enter the AAD endpoint URL here}"
# The tenant id of Azure Active Directory
SETX PCS_AAD_TENANT "{enter the tenant id of AAD here}"
# The secret of intended application audience
SETX PCS_AAD_APPSECRET "{enter the secret of AAD application here}"
# Azure Resource Manager endpoint url, e.g. https://management.azure.com/
SETX PCS_ARM_ENDPOINT_URL "{enter the endpoint URL of Azure Resource Manager here}"

Просмотреть файл

@ -60,6 +60,10 @@ run_in_sandbox() {
-p 9001:9001 \
-e PCS_AUTH_ISSUER \
-e PCS_AUTH_AUDIENCE \
-e PCS_AAD_ENDPOINT_URL \
-e PCS_AAD_TENANT \
-e PCS_AAD_APPSECRET \
-e PCS_ARM_ENDPOINT_URL \
-v "$PCS_CACHE/sandbox/.config:/root/.config" \
-v "$PCS_CACHE/sandbox/.dotnet:/root/.dotnet" \
-v "$PCS_CACHE/sandbox/.nuget:/root/.nuget" \

Просмотреть файл

@ -63,6 +63,10 @@ IF "%1"=="--in-sandbox" GOTO :RunInSandbox
-p 9001:9001 ^
-e PCS_AUTH_ISSUER ^
-e PCS_AUTH_AUDIENCE ^
-e PCS_AAD_ENDPOINT_URL ^
-e PCS_AAD_TENANT ^
-e PCS_AAD_APPSECRET ^
-e PCS_ARM_ENDPOINT_URL ^
-v %PCS_CACHE%\sandbox\.config:/root/.config ^
-v %PCS_CACHE%\sandbox\.dotnet:/root/.dotnet ^
-v %PCS_CACHE%\sandbox\.nuget:/root/.nuget ^

Просмотреть файл

@ -80,6 +80,13 @@ variables [here](#configuration-and-environment-variables).
* `PCS_AUTH_WEBSERVICE_URL` = the url for
the [Auth Webservice](https://github.com/Azure/pcs-auth-dotnet)
used for key value storage
* `PCS_OFFICE365_CONNECTION_URL` (Optional) = the url for the Office 365 Logic App connector
* `PCS_SOLUTION_NAME` (Optional) = The name of the resource group for the solution. Used only if
using logic apps for actions.
* `PCS_SUBSCRIPTION_ID`(Optional) = The subscription id for the solution. Used only if
using logic apps for actions.
* `PCS_ARM_ENDPOINT_URL` (Optional) = the url for the Azure management APIs. Used only if
using logic apps for actions.
## Running the service with Visual Studio or VS Code
@ -98,6 +105,10 @@ variables [here](#configuration-and-environment-variables).
1. `PCS_TELEMETRY_WEBSERVICE_URL` = http://localhost:9004/v1
1. `PCS_AZUREMAPS_KEY` = static
1. `PCS_AUTH_WEBSERVICE_URL` = http://localhost:9001/v1
1. `PCS_OFFICE365_CONNECTION_URL` (Optional)
1. `PCS_SOLUTION_NAME` (Optional)
1. `PCS_SUBSCRIPTION_ID`(Optional)
1. `PCS_ARM_ENDPOINT_URL` (Optional)
1. Start the WebService project (e.g. press F5).
1. Use an HTTP client such as [Postman][postman-url], to exercise the
[RESTful API](https://github.com/Azure/pcs-config-dotnet/wiki/API-Specs).
@ -113,6 +124,10 @@ More information on environment variables
1. `PCS_TELEMETRY_WEBSERVICE_URL` = http://localhost:9004/v1
1. `PCS_AZUREMAPS_KEY` = static
1. `PCS_AUTH_WEBSERVICE_URL` = http://localhost:9001/v1
1. `PCS_OFFICE365_CONNECTION_URL` (Optional)
1. `PCS_SOLUTION_NAME` (Optional)
1. `PCS_SUBSCRIPTION_ID`(Optional)
1. `PCS_ARM_ENDPOINT_URL` (Optional)
1. Use the scripts in the [scripts](scripts) folder for many frequent tasks:
* `build`: compile all the projects and run the tests.
* `compile`: compile all the projects.

Просмотреть файл

@ -0,0 +1,117 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.External;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Http;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
using Moq;
using Services.Test.helpers;
using Xunit;
using IUserManagementClient = Microsoft.Azure.IoTSolutions.UIConfig.Services.External.IUserManagementClient;
namespace Services.Test
{
public class AzureResourceManagerClientTest
{
private const string MOCK_SUBSCRIPTION_ID = @"123456abcd";
private const string MOCK_RESOURCE_GROUP = @"example-name";
private const string MOCK_ARM_ENDPOINT_URL = @"https://management.azure.com";
private const string MOCK_API_VERSION = @"2016-06-01";
private readonly string logicAppTestConnectionUrl;
private readonly Mock<IHttpClient> mockHttpClient;
private readonly Mock<IUserManagementClient> mockUserManagementClient;
private readonly AzureResourceManagerClient client;
public AzureResourceManagerClientTest()
{
this.mockHttpClient = new Mock<IHttpClient>();
this.mockUserManagementClient = new Mock<IUserManagementClient>();
this.client = new AzureResourceManagerClient(
this.mockHttpClient.Object,
new ServicesConfig
{
SubscriptionId = MOCK_SUBSCRIPTION_ID,
ResourceGroup = MOCK_RESOURCE_GROUP,
ArmEndpointUrl = MOCK_ARM_ENDPOINT_URL,
ManagementApiVersion = MOCK_API_VERSION
},
this.mockUserManagementClient.Object);
this.logicAppTestConnectionUrl = $"{MOCK_ARM_ENDPOINT_URL}" +
$"/subscriptions/{MOCK_SUBSCRIPTION_ID}/" +
$"resourceGroups/{MOCK_RESOURCE_GROUP}/" +
"providers/Microsoft.Web/connections/" +
"office365-connector/extensions/proxy/testconnection?" +
$"api-version={MOCK_API_VERSION}";
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetOffice365IsEnabled_ReturnsTrueIfEnabled()
{
// Arrange
var response = new HttpResponse
{
StatusCode = HttpStatusCode.OK,
IsSuccessStatusCode = true
};
this.mockHttpClient
.Setup(x => x.GetAsync(It.IsAny<IHttpRequest>()))
.ReturnsAsync(response);
// Act
var result = await this.client.IsOffice365EnabledAsync();
// Assert
this.mockHttpClient
.Verify(x => x.GetAsync(
It.Is<IHttpRequest>(r => r.Check(
this.logicAppTestConnectionUrl))), Times.Once);
Assert.True(result);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetOffice365IsEnabled_ReturnsFalseIfDisabled()
{
// Arrange
var response = new HttpResponse
{
StatusCode = HttpStatusCode.NotFound,
IsSuccessStatusCode = false
};
this.mockHttpClient
.Setup(x => x.GetAsync(It.IsAny<IHttpRequest>()))
.ReturnsAsync(response);
// Act
var result = await this.client.IsOffice365EnabledAsync();
// Assert
this.mockHttpClient
.Verify(x => x.GetAsync(
It.Is<IHttpRequest>(r => r.Check(
this.logicAppTestConnectionUrl))), Times.Once);
Assert.False(result);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetOffice365IsEnabled_ThrowsIfNotAuthorizd()
{
// Arrange
this.mockUserManagementClient
.Setup(x => x.GetTokenAsync())
.ThrowsAsync(new NotAuthorizedException());
// Act & Assert
await Assert.ThrowsAsync<NotAuthorizedException>(async () => await this.client.IsOffice365EnabledAsync());
}
}
}

Просмотреть файл

@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
@ -104,5 +103,38 @@ namespace Services.Test
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await this.client.GetAllowedActionsAsync(userObjectId, roles));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetToken_ReturnsValue()
{
// Arrange
var token = new TokenApiModel()
{
AccessToken = "1234ExampleToken",
AccessTokenType = "Bearer",
Audience = "https://management.azure.com/",
Authority = "https://login.microsoftonline.com/12345/"
};
var response = new HttpResponse
{
StatusCode = HttpStatusCode.OK,
IsSuccessStatusCode = true,
Content = JsonConvert.SerializeObject(token)
};
this.mockHttpClient
.Setup(x => x.GetAsync(It.IsAny<IHttpRequest>()))
.ReturnsAsync(response);
// Act
var result = await this.client.GetTokenAsync();
// Assert
this.mockHttpClient
.Verify(x => x.GetAsync(It.Is<IHttpRequest>(r => r.Check($"{MOCK_SERVICE_URI}/users/default/token"))), Times.Once);
Assert.Equal(token.AccessToken, result);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.External;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services
{
public interface IActions
{
Task<List<IActionSettings>> GetListAsync();
}
public class Actions : IActions
{
private readonly IAzureResourceManagerClient resourceManagerClient;
private readonly IServicesConfig servicesConfig;
private readonly ILogger log;
public Actions(
IAzureResourceManagerClient resourceManagerClient,
IServicesConfig servicesConfig,
ILogger log)
{
this.resourceManagerClient = resourceManagerClient;
this.servicesConfig = servicesConfig;
this.log = log;
}
public async Task <List<IActionSettings>> GetListAsync()
{
var result = new List<IActionSettings>();
// Add Email Action Settings
var emailActionSettings = new EmailActionSettings(
this.resourceManagerClient,
this.servicesConfig,
this.log);
await emailActionSettings.InitializeAsync();
result.Add(emailActionSettings);
return result;
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions
{
/// <summary>
/// This exception is thrown when the user or the application
/// is not authorized to perform the action.
/// </summary>
public class NotAuthorizedException : Exception
{
public NotAuthorizedException() : base()
{
}
public NotAuthorizedException(string message) : base(message)
{
}
public NotAuthorizedException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

85
config/Services/External/AzureResourceManagerClient.cs поставляемый Normal file
Просмотреть файл

@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Http;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
{
public interface IAzureResourceManagerClient
{
Task<bool> IsOffice365EnabledAsync();
}
public class AzureResourceManagerClient : IAzureResourceManagerClient
{
private readonly IHttpClient httpClient;
private readonly IUserManagementClient userManagementClient;
private readonly IServicesConfig config;
public AzureResourceManagerClient(
IHttpClient httpClient,
IServicesConfig config,
IUserManagementClient userManagementClient)
{
this.httpClient = httpClient;
this.userManagementClient = userManagementClient;
this.config = config;
}
public async Task<bool> IsOffice365EnabledAsync()
{
if (string.IsNullOrEmpty(this.config.SubscriptionId) ||
string.IsNullOrEmpty(this.config.ResourceGroup) ||
string.IsNullOrEmpty(this.config.ArmEndpointUrl))
{
throw new InvalidConfigurationException("Subscription Id, Resource Group, and Arm Endpoint Url must be specified" +
"in the environment variable configuration for this " +
"solution in order to use this API.");
}
var logicAppTestConnectionUri = this.config.ArmEndpointUrl +
$"/subscriptions/{this.config.SubscriptionId}/" +
$"resourceGroups/{this.config.ResourceGroup}/" +
"providers/Microsoft.Web/connections/" +
"office365-connector/extensions/proxy/testconnection?" +
$"api-version={this.config.ManagementApiVersion}";
var request = await this.CreateRequest(logicAppTestConnectionUri);
var response = await this.httpClient.GetAsync(request);
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new NotAuthorizedException("The application is not authorized and has not been " +
"assigned contributor permissions for the subscription. Go to the Azure portal and " +
"assign the application as a contributor in order to retrieve the token.");
}
return response.IsSuccessStatusCode;
}
private async Task<HttpRequest> CreateRequest(string uri, IEnumerable<string> content = null)
{
var request = new HttpRequest();
request.SetUriFromString(uri);
if (uri.ToLowerInvariant().StartsWith("https:"))
{
request.Options.AllowInsecureSSLServer = true;
}
if (content != null)
{
request.SetContent(content);
}
var token = await this.userManagementClient.GetTokenAsync();
request.Headers.Add("Authorization", "Bearer " + token);
return request;
}
}
}

24
config/Services/External/TokenApiModel.cs поставляемый Normal file
Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
{
public class TokenApiModel
{
[JsonProperty(PropertyName = "Audience", Order = 10)]
public string Audience { get; set; }
[JsonProperty(PropertyName = "AccessTokenType", Order = 20)]
public string AccessTokenType { get; set; }
[JsonProperty(PropertyName = "AccessToken", Order = 30)]
public string AccessToken { get; set; }
[JsonProperty(PropertyName = "Authority", Order = 40)]
public string Authority { get; set; }
[JsonProperty(PropertyName = "ExpiresOn", Order = 50)]
public DateTimeOffset ExpiresOn { get; set; }
}
}

Просмотреть файл

@ -15,6 +15,8 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
public interface IUserManagementClient
{
Task<IEnumerable<string>> GetAllowedActionsAsync(string userObjectId, IEnumerable<string> roles);
Task<string> GetTokenAsync();
}
public class UserManagementClient : IUserManagementClient
@ -22,6 +24,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
private readonly IHttpClient httpClient;
private readonly ILogger log;
private readonly string serviceUri;
private const string DEFAULT_USER_ID = "default";
public UserManagementClient(IHttpClient httpClient, IServicesConfig config, ILogger logger)
{
@ -39,6 +42,20 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
return JsonConvert.DeserializeObject<IEnumerable<string>>(response.Content);
}
public async Task<string> GetTokenAsync()
{
// Note: The DEFAULT_USER_ID is set to any value. The user management service doesn't
// currently use the user ID information, but if this API is updated in the future, we
// will need to grab the user ID from the request JWT token and pass in here.
var request = this.CreateRequest($"users/{DEFAULT_USER_ID}/token");
var response = await this.httpClient.GetAsync(request);
this.CheckStatusCode(response, request);
var tokenResponse = JsonConvert.DeserializeObject<TokenApiModel>(response.Content);
return tokenResponse.AccessToken;
}
private HttpRequest CreateRequest(string path, IEnumerable<string> content = null)
{
var request = new HttpRequest();
@ -74,8 +91,13 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.External
{
case HttpStatusCode.NotFound:
throw new ResourceNotFoundException($"{response.Content}, request URL = {request.Uri}");
case HttpStatusCode.Forbidden:
throw new NotAuthorizedException("The user or the application is not authorized to make the " +
$"request to the user management service, content = {response.Content}, " +
$"request URL = {request.Uri}");
default:
throw new HttpRequestException($"Http request failed, status code = {response.StatusCode}, content = {response.Content}, request URL = {request.Uri}");
throw new HttpRequestException($"Http request failed, status code = {response.StatusCode}, " +
"content = {response.Content}, request URL = {request.Uri}");
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions
{
public interface IActionSettings
{
ActionType Type { get; }
// Note: This should always be initialized as a case-insensitive dictionary
IDictionary<string, object> Settings { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions
{
public enum ActionType
{
Email
}
}

Просмотреть файл

@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.External;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions
{
public class EmailActionSettings : IActionSettings
{
private const string IS_ENABLED_KEY = "IsEnabled";
private const string OFFICE365_CONNECTOR_URL_KEY = "Office365ConnectorUrl";
private const string APP_PERMISSIONS_KEY = "ApplicationPermissionsAssigned";
private readonly IAzureResourceManagerClient resourceManagerClient;
private readonly IServicesConfig servicesConfig;
private readonly ILogger log;
public ActionType Type { get; }
public IDictionary<string, object> Settings { get; set; }
// In order to initialize all settings, call InitializeAsync
// to retrieve all settings due to async call to logic app
public EmailActionSettings(
IAzureResourceManagerClient resourceManagerClient,
IServicesConfig servicesConfig,
ILogger log)
{
this.resourceManagerClient = resourceManagerClient;
this.servicesConfig = servicesConfig;
this.log = log;
this.Type = ActionType.Email;
this.Settings = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
public async Task InitializeAsync()
{
// Check signin status of Office 365 Logic App Connector
var office365IsEnabled = false;
var applicationPermissionsAssigned = true;
try
{
office365IsEnabled = await this.resourceManagerClient.IsOffice365EnabledAsync();
}
catch (NotAuthorizedException notAuthorizedException)
{
// If there is a 403 Not Authorized exception, it means the application has not
// been given owner permissions to make the isEnabled check. This can be configured
// by an owner in the Azure Portal.
applicationPermissionsAssigned = false;
this.log.Debug("The application is not authorized and has not been " +
"assigned owner permissions for the subscription. Go to the Azure portal and " +
"assign the application as an owner in order to retrieve the token.", () => new { notAuthorizedException });
}
this.Settings.Add(IS_ENABLED_KEY, office365IsEnabled);
this.Settings.Add(APP_PERMISSIONS_KEY, applicationPermissionsAssigned);
// Get Url for Office 365 Logic App Connector setup in portal
// for display on the webui for one-time setup.
this.Settings.Add(OFFICE365_CONNECTOR_URL_KEY, this.servicesConfig.Office365LogicAppUrl);
this.log.Debug("Email Action Settings Retrieved. Email setup status: " + office365IsEnabled, () => new { this.Settings });
}
}
}

Просмотреть файл

@ -11,6 +11,11 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime
string SeedTemplate { get; }
string AzureMapsKey { get; }
string UserManagementApiUrl { get; }
string Office365LogicAppUrl { get; }
string ResourceGroup { get; }
string SubscriptionId { get; }
string ManagementApiVersion { get; }
string ArmEndpointUrl { get; }
}
public class ServicesConfig : IServicesConfig
@ -22,5 +27,10 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime
public string SeedTemplate { get; set; }
public string AzureMapsKey { get; set; }
public string UserManagementApiUrl { get; set; }
public string Office365LogicAppUrl { get; set; }
public string ResourceGroup { get; set; }
public string SubscriptionId { get; set; }
public string ManagementApiVersion { get; set; }
public string ArmEndpointUrl { get; set; }
}
}

Просмотреть файл

@ -1,9 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.UIConfig.Services;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.External;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers;
using Moq;
using WebService.Test.helpers;
@ -14,13 +19,21 @@ namespace WebService.Test.Controllers
public class SolutionControllerTest
{
private readonly Mock<IStorage> mockStorage;
private readonly Mock<IActions> mockActions;
private readonly Mock<ILogger> mockLogger;
private readonly Mock<IAzureResourceManagerClient> mockResourceManagementClient;
private readonly SolutionSettingsController controller;
private readonly Random rand;
public SolutionControllerTest()
{
this.mockStorage = new Mock<IStorage>();
this.controller = new SolutionSettingsController(this.mockStorage.Object);
this.mockActions = new Mock<IActions>();
this.mockLogger = new Mock<ILogger>();
this.mockResourceManagementClient = new Mock<IAzureResourceManagerClient>();
this.controller = new SolutionSettingsController(
this.mockStorage.Object,
this.mockActions.Object);
this.rand = new Random();
}
@ -204,5 +217,34 @@ namespace WebService.Test.Controllers
Assert.Equal("False", mockContext.GetHeader(Logo.IS_DEFAULT_HEADER));
}
}
[Fact]
public async Task GetActionsReturnsListOfActions()
{
// Arrange
using (var mockContext = new MockHttpContext())
{
this.controller.ControllerContext.HttpContext = mockContext.Object;
var config = new ServicesConfig();
var action = new EmailActionSettings(this.mockResourceManagementClient.Object, config, this.mockLogger.Object);
var actionsList = new List<IActionSettings>
{
action
};
this.mockActions
.Setup(x => x.GetListAsync())
.ReturnsAsync(actionsList);
// Act
var result = await this.controller.GetActionsSettingsAsync();
// Assert
this.mockActions.Verify(x => x.GetListAsync(), Times.Once);
Assert.NotEmpty(result.Items);
Assert.Equal(actionsList.Count, result.Items.Count);
}
}
}
}

Просмотреть файл

@ -80,6 +80,8 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService
builder.RegisterInstance(httpClient).As<IHttpClient>().SingleInstance();
builder.RegisterType<UserManagementClient>().As<IUserManagementClient>().SingleInstance();
builder.RegisterType<AzureResourceManagerClient>().As<IAzureResourceManagerClient>().SingleInstance();
}
private static void RegisterFactory(IContainer container)

Просмотреть файл

@ -49,6 +49,13 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.Runtime
private const string USER_MANAGEMENT_KEY = "UserManagementService:";
private const string USER_MANAGEMENT_URL_KEY = USER_MANAGEMENT_KEY + "webservice_url";
private const string ACTIONS_KEY = APPLICATION_KEY + "Actions:";
private const string OFFICE365_LOGIC_APP_URL_KEY = ACTIONS_KEY + "office365_logic_app_url";
private const string RESOURCE_GROUP_KEY = ACTIONS_KEY + "resource_group";
private const string SUBSCRIPTION_ID_KEY = ACTIONS_KEY + "subscription_id";
private const string MANAGEMENT_API_VERSION_KEY = ACTIONS_KEY + "management_api_version";
private const string ARM_ENDPOINT_URL_KEY = ACTIONS_KEY + "arm_endpoint_url";
public int Port { get; }
public IServicesConfig ServicesConfig { get; }
public IClientAuthConfig ClientAuthConfig { get; }
@ -65,7 +72,12 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.Runtime
SolutionType = configData.GetString(SOLUTION_TYPE_KEY),
SeedTemplate = configData.GetString(SEED_TEMPLATE_KEY),
AzureMapsKey = configData.GetString(AZURE_MAPS_KEY),
UserManagementApiUrl = configData.GetString(USER_MANAGEMENT_URL_KEY)
UserManagementApiUrl = configData.GetString(USER_MANAGEMENT_URL_KEY),
Office365LogicAppUrl = configData.GetString(OFFICE365_LOGIC_APP_URL_KEY),
ResourceGroup = configData.GetString(RESOURCE_GROUP_KEY),
SubscriptionId = configData.GetString(SUBSCRIPTION_ID_KEY),
ManagementApiVersion = configData.GetString(MANAGEMENT_API_VERSION_KEY),
ArmEndpointUrl = configData.GetString(ARM_ENDPOINT_URL_KEY)
};
this.ClientAuthConfig = new ClientAuthConfig

Просмотреть файл

@ -16,9 +16,23 @@ webservice_url = "${PCS_DEVICESIMULATION_WEBSERVICE_URL}"
[TelemetryService]
webservice_url = "${PCS_TELEMETRY_WEBSERVICE_URL}"
[UserManagementService]
webservice_url = "${PCS_AUTH_WEBSERVICE_URL}"
[ConfigService:Actions]
;; Url for the Office365 Logic App Connector in the Azure portal
office365_logic_app_url = "${PCS_OFFICE365_CONNECTION_URL}"
;; Resource group name, arm endpoint, and Subscription ID for the
;; Office365 Logic App Connector API call.
resource_group = "${PCS_SOLUTION_NAME}"
subscription_id = "${PCS_SUBSCRIPTION_ID}"
arm_endpoint_url = "${PCS_ARM_ENDPOINT_URL}"
;; api version for the azure management apis
management_api_version = "2016-06-01"
[ConfigService:ClientAuth]
;; Current auth type, only "JWT" is currently supported.
auth_type="JWT"

Просмотреть файл

@ -19,12 +19,14 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpGet]
[Authorize("ReadAll")]
public async Task<DeviceGroupListApiModel> GetAllAsync()
{
return new DeviceGroupListApiModel(await this.storage.GetAllDeviceGroupsAsync());
}
[HttpGet("{id}")]
[Authorize("ReadAll")]
public async Task<DeviceGroupApiModel> GetAsync(string id)
{
return new DeviceGroupApiModel(await this.storage.GetDeviceGroupAsync(id));

Просмотреть файл

@ -24,6 +24,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpGet]
[Authorize("ReadAll")]
public async Task<PackageListApiModel> GetAllAsync()
{
return new PackageListApiModel(await this.storage.GetPackagesAsync());
@ -46,6 +47,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpGet("{id}")]
[Authorize("ReadAll")]
public async Task<PackageApiModel> GetAsync(string id)
{
if (string.IsNullOrEmpty(id))

Просмотреть файл

@ -18,6 +18,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpPost]
[Authorize("ReadAll")]
public async Task PostAsync()
{
await this.seed.TrySeedAsync();

Просмотреть файл

@ -6,7 +6,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.UIConfig.Services;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Filters;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
@ -15,26 +17,32 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
public class SolutionSettingsController : Controller
{
private readonly IStorage storage;
private readonly IActions actions;
private static readonly string ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
public SolutionSettingsController(IStorage storage)
public SolutionSettingsController(IStorage storage, IActions actions)
{
this.storage = storage;
this.actions = actions;
}
[HttpGet("solution-settings/theme")]
[Authorize("ReadAll")]
public async Task<object> GetThemeAsync()
{
return await this.storage.GetThemeAsync();
}
[HttpPut("solution-settings/theme")]
[Authorize("ReadAll")]
public async Task<object> SetThemeAsync([FromBody] object theme)
{
return await this.storage.SetThemeAsync(theme);
}
[HttpGet("solution-settings/logo")]
[Authorize("ReadAll")]
public async Task GetLogoAsync()
{
var model = await this.storage.GetLogoAsync();
@ -42,6 +50,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpPut("solution-settings/logo")]
[Authorize("ReadAll")]
public async Task SetLogoAsync()
{
MemoryStream memoryStream = new MemoryStream();
@ -68,6 +77,13 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
this.SetImageResponse(response);
}
[HttpGet("solution-settings/actions")]
public async Task<ActionSettingsListApiModel> GetActionsSettingsAsync()
{
var actions = await this.actions.GetListAsync();
return new ActionSettingsListApiModel(actions);
}
private void SetImageResponse(Logo model)
{
if(model.Name != null)

Просмотреть файл

@ -17,6 +17,7 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
this.log = logger;
}
[Authorize("ReadAll")]
public StatusApiModel Get()
{
// TODO: calculate the actual service status

Просмотреть файл

@ -18,12 +18,14 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
}
[HttpGet("user-settings/{id}")]
[Authorize("ReadAll")]
public async Task<object> GetUserSettingAsync(string id)
{
return await this.storage.GetUserSetting(id);
}
[HttpPut("user-settings/{id}")]
[Authorize("ReadAll")]
public async Task<object> SetUserSettingAsync(string id, [FromBody] object setting)
{
return await this.storage.SetUserSetting(id, setting);

Просмотреть файл

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.Auth;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Exceptions;
using Newtonsoft.Json;
@ -52,7 +51,8 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Filters
{
context.Result = this.GetResponse(HttpStatusCode.InternalServerError, context.Exception);
}
else if (context.Exception is NotAuthorizedException)
else if (context.Exception is Auth.NotAuthorizedException ||
context.Exception is Services.Exceptions.NotAuthorizedException)
{
context.Result = this.GetResponse(HttpStatusCode.Forbidden, context.Exception);
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models
{
public class ActionSettingsApiModel
{
[JsonProperty("Type")]
public string Type { get; set; }
[JsonProperty("Settings")]
public IDictionary<string, object> Settings { get; set; }
public ActionSettingsApiModel()
{
this.Type = ActionType.Email.ToString();
this.Settings = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
public ActionSettingsApiModel(IActionSettings actionSettings)
{
this.Type = actionSettings.Type.ToString();
this.Settings = new Dictionary<string, object>(
actionSettings.Settings,
StringComparer.OrdinalIgnoreCase);
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models.Actions;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models
{
public class ActionSettingsListApiModel
{
[JsonProperty("Items")]
public List<ActionSettingsApiModel> Items { get; set; }
[JsonProperty("$metadata")]
public Dictionary<string, string> Metadata { get; set; }
public ActionSettingsListApiModel(List<IActionSettings> actionSettingsList)
{
this.Items = new List<ActionSettingsApiModel>();
foreach (var actionSettings in actionSettingsList)
{
this.Items.Add(new ActionSettingsApiModel(actionSettings));
}
this.Metadata = new Dictionary<string, string>
{
{ "$type", $"ActionSettingsList;{Version.NUMBER}" },
{ "$url", $"/{Version.PATH}/solution-settings/actions" }
};
}
}
}

Просмотреть файл

@ -1,81 +1,89 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebService", "WebService\WebService.csproj", "{F305CAD5-BE38-4391-A692-580C526A0CB1}"
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2008
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebService", "WebService\WebService.csproj", "{F305CAD5-BE38-4391-A692-580C526A0CB1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebService.Test", "WebService.Test\WebService.Test.csproj", "{6A4D86AD-56D0-421B-B9B2-2EBB34A5BBBC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebService.Test", "WebService.Test\WebService.Test.csproj", "{6A4D86AD-56D0-421B-B9B2-2EBB34A5BBBC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{8289AFAE-0933-40EF-9D19-6FFA2DB04837}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
.travis.yml = .travis.yml
CONTRIBUTING.md = CONTRIBUTING.md
LICENSE = LICENSE
README.md = README.md
DEVELOPMENT.md = DEVELOPMENT.md
pcs-config.sln.DotSettings = pcs-config.sln.DotSettings
EndProjectSection
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
.travis.yml = .travis.yml
docs\API_SPEC_ACTION_SETTINGS.md = docs\API_SPEC_ACTION_SETTINGS.md
CONTRIBUTING.md = CONTRIBUTING.md
DEVELOPMENT.md = DEVELOPMENT.md
LICENSE = LICENSE
pcs-config.sln.DotSettings = pcs-config.sln.DotSettings
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{786A5B9B-98EE-4B73-9FE3-2080B70007AF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "Services\Services.csproj", "{786A5B9B-98EE-4B73-9FE3-2080B70007AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Test", "Services.Test\Services.Test.csproj", "{1BC54189-1DF9-448E-AE5F-66EB2EF51D82}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Test", "Services.Test\Services.Test.csproj", "{1BC54189-1DF9-448E-AE5F-66EB2EF51D82}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{7357A079-B54D-490F-AA91-1B7BDAFE5E3F}"
ProjectSection(SolutionItems) = preProject
scripts\.functions.sh = scripts\.functions.sh
scripts\build = scripts\build
scripts\build.cmd = scripts\build.cmd
scripts\clean-up = scripts\clean-up
scripts\clean-up.cmd = scripts\clean-up.cmd
scripts\compile = scripts\compile
scripts\compile.cmd = scripts\compile.cmd
scripts\run = scripts\run
scripts\run.cmd = scripts\run.cmd
scripts\env-vars-check = scripts\env-vars-check
scripts\env-vars-check.cmd = scripts\env-vars-check.cmd
scripts\env-vars-setup = scripts\env-vars-setup
scripts\travis = scripts\travis
scripts\travis.cmd = scripts\travis.cmd
scripts\env-vars-setup.cmd = scripts\env-vars-setup.cmd
EndProjectSection
ProjectSection(SolutionItems) = preProject
scripts\.functions.sh = scripts\.functions.sh
scripts\build = scripts\build
scripts\build.cmd = scripts\build.cmd
scripts\clean-up = scripts\clean-up
scripts\clean-up.cmd = scripts\clean-up.cmd
scripts\compile = scripts\compile
scripts\compile.cmd = scripts\compile.cmd
scripts\env-vars-check = scripts\env-vars-check
scripts\env-vars-check.cmd = scripts\env-vars-check.cmd
scripts\env-vars-setup = scripts\env-vars-setup
scripts\env-vars-setup.cmd = scripts\env-vars-setup.cmd
scripts\run = scripts\run
scripts\run.cmd = scripts\run.cmd
scripts\travis = scripts\travis
scripts\travis.cmd = scripts\travis.cmd
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{FE64576E-E29D-4463-ABDA-93DB429A5F2F}"
ProjectSection(SolutionItems) = preProject
scripts\git\pre-commit-runner-no-sandbox.sh = scripts\git\pre-commit-runner-no-sandbox.sh
scripts\git\pre-commit-runner-with-sandbox.sh = scripts\git\pre-commit-runner-with-sandbox.sh
scripts\git\pre-commit.sh = scripts\git\pre-commit.sh
scripts\git\setup = scripts\git\setup
scripts\git\setup.cmd = scripts\git\setup.cmd
scripts\git\fix-perms.sh = scripts\git\fix-perms.sh
scripts\git\.functions.sh = scripts\git\.functions.sh
EndProjectSection
ProjectSection(SolutionItems) = preProject
scripts\git\.functions.sh = scripts\git\.functions.sh
scripts\git\fix-perms.sh = scripts\git\fix-perms.sh
scripts\git\pre-commit-runner-no-sandbox.sh = scripts\git\pre-commit-runner-no-sandbox.sh
scripts\git\pre-commit-runner-with-sandbox.sh = scripts\git\pre-commit-runner-with-sandbox.sh
scripts\git\pre-commit.sh = scripts\git\pre-commit.sh
scripts\git\setup = scripts\git\setup
scripts\git\setup.cmd = scripts\git\setup.cmd
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{DB8AF4E5-71A1-4650-A881-9F08FCB53DAA}"
ProjectSection(SolutionItems) = preProject
scripts\docker\.dockerignore = scripts\docker\.dockerignore
scripts\docker\Dockerfile = scripts\docker\Dockerfile
scripts\docker\build = scripts\docker\build
scripts\docker\build.cmd = scripts\docker\build.cmd
scripts\docker\run = scripts\docker\run
scripts\docker\run.cmd = scripts\docker\run.cmd
scripts\docker\publish = scripts\docker\publish
scripts\docker\publish.cmd = scripts\docker\publish.cmd
scripts\docker\docker-compose.yml = scripts\docker\docker-compose.yml
EndProjectSection
ProjectSection(SolutionItems) = preProject
scripts\docker\.dockerignore = scripts\docker\.dockerignore
scripts\docker\build = scripts\docker\build
scripts\docker\build.cmd = scripts\docker\build.cmd
scripts\docker\docker-compose.yml = scripts\docker\docker-compose.yml
scripts\docker\Dockerfile = scripts\docker\Dockerfile
scripts\docker\publish = scripts\docker\publish
scripts\docker\publish.cmd = scripts\docker\publish.cmd
scripts\docker\run = scripts\docker\run
scripts\docker\run.cmd = scripts\docker\run.cmd
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8BAA54A8-9C80-4446-83DD-CEBBC7B7BCD2}"
ProjectSection(SolutionItems) = preProject
scripts\docker\content\run.sh = scripts\docker\content\run.sh
scripts\docker\content\README.md = scripts\docker\content\README.md
EndProjectSection
ProjectSection(SolutionItems) = preProject
scripts\docker\content\README.md = scripts\docker\content\README.md
scripts\docker\content\run.sh = scripts\docker\content\run.sh
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{9FBD3528-31A7-4289-9BA5-341C253958CB}"
ProjectSection(SolutionItems) = preProject
.github\CODEOWNERS = .github\CODEOWNERS
.github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
.github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md
EndProjectSection
ProjectSection(SolutionItems) = preProject
.github\CODEOWNERS = .github\CODEOWNERS
.github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
.github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{251AB4CA-6D8C-4A31-AF7E-28705E32C79E}"
ProjectSection(SolutionItems) = preProject
docs\API_SPEC_ACTION_SETTINGS.md = docs\API_SPEC_ACTION_SETTINGS.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -103,6 +111,17 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7357A079-B54D-490F-AA91-1B7BDAFE5E3F} = {8289AFAE-0933-40EF-9D19-6FFA2DB04837}
{FE64576E-E29D-4463-ABDA-93DB429A5F2F} = {7357A079-B54D-490F-AA91-1B7BDAFE5E3F}
{DB8AF4E5-71A1-4650-A881-9F08FCB53DAA} = {7357A079-B54D-490F-AA91-1B7BDAFE5E3F}
{8BAA54A8-9C80-4446-83DD-CEBBC7B7BCD2} = {DB8AF4E5-71A1-4650-A881-9F08FCB53DAA}
{9FBD3528-31A7-4289-9BA5-341C253958CB} = {8289AFAE-0933-40EF-9D19-6FFA2DB04837}
{251AB4CA-6D8C-4A31-AF7E-28705E32C79E} = {8289AFAE-0933-40EF-9D19-6FFA2DB04837}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ED657AC8-0BF3-44CC-A093-2A1AE174AF05}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
Policies = $0
$0.DotNetNamingPolicy = $1
@ -113,11 +132,4 @@ Global
$3.scope = text/x-csharp
$0.VersionControlPolicy = $4
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7357A079-B54D-490F-AA91-1B7BDAFE5E3F} = {8289AFAE-0933-40EF-9D19-6FFA2DB04837}
{FE64576E-E29D-4463-ABDA-93DB429A5F2F} = {7357A079-B54D-490F-AA91-1B7BDAFE5E3F}
{DB8AF4E5-71A1-4650-A881-9F08FCB53DAA} = {7357A079-B54D-490F-AA91-1B7BDAFE5E3F}
{8BAA54A8-9C80-4446-83DD-CEBBC7B7BCD2} = {DB8AF4E5-71A1-4650-A881-9F08FCB53DAA}
{9FBD3528-31A7-4289-9BA5-341C253958CB} = {8289AFAE-0933-40EF-9D19-6FFA2DB04837}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,41 @@
API Specification - Action Settings
======================================
## Get a list of actions settings
The list of configuration settings for all action types.
The ApplicationPermissionsAssigned attribute indicates whether or not the application has
been given "Contributor" permissions in order to make management API calls. If the application
does not have "Contributor" permissions, then users will need to check on resources manually from
the Azure portal. This can happen when the user deploying the application does not have "Owner"
permissions in order to assign the application the necessry permissions.
Request:
```
GET /v1/solution-settings/actions
```
Response:
```
200 OK
Content-Type: application/json
```
```json
{
"Items": [
{
"Type": "Email",
"Settings": {
"IsEnabled": false,
"ApplicationPermissionsAssigned": false,
"Office365ConnectorUrl": "https://portal.azure.com/#@{tenant}/resource/subscriptions/{subscription}/resourceGroups/{resource-group}/providers/Microsoft.Web/connections/office365-connector/edit"
}
}
],
"$metadata": {
"$type": "ActionSettingsList;1",
"$url": "/v1/solution-settings/actions"
}
}
```

Просмотреть файл

@ -20,6 +20,10 @@ services:
- PCS_TELEMETRY_WEBSERVICE_URL=http://telemetry:9004/v1
- PCS_DEVICESIMULATION_WEBSERVICE_URL=http://devicesimlation:9003/v1
- PCS_AUTH_WEBSERVICE_URL=http://auth:9001/v1
- PCS_OFFICE365_CONNECTION_URL
- PCS_SOLUTION_NAME
- PCS_SUBSCRIPTION_ID
- PCS_ARM_ENDPOINT_URL
auth:
image: azureiotpcs/pcs-auth-dotnet:testing

Просмотреть файл

@ -17,6 +17,10 @@ run_container() {
-e PCS_DEVICESIMULATION_WEBSERVICE_URL \
-e PCS_TELEMETRY_WEBSERVICE_URL \
-e PCS_AUTH_WEBSERVICE_URL \
-e PCS_OFFICE365_CONNECTION_URL \
-e PCS_SOLUTION_NAME \
-e PCS_SUBSCRIPTION_ID \
-e PCS_ARM_ENDPOINT_URL \
"$DOCKER_IMAGE:testing"
}

Просмотреть файл

@ -23,6 +23,10 @@ docker run -it -p 9005:9005 ^
-e PCS_DEVICESIMULATION_WEBSERVICE_URL ^
-e PCS_TELEMETRY_WEBSERVICE_URL ^
-e PCS_AUTH_WEBSERVICE_URL ^
-e PCS_OFFICE365_CONNECTION_URL ^
-e PCS_SOLUTION_NAME ^
-e PCS_SUBSCRIPTION_ID ^
-e PCS_ARM_ENDPOINT_URL ^
%DOCKER_IMAGE%:testing
:: - - - - - - - - - - - - - -

Просмотреть файл

@ -36,3 +36,20 @@ if [[ -z "$PCS_AUTH_WEBSERVICE_URL" ]]; then
echo "Error: the PCS_AUTH_WEBSERVICE_URL environment variable is not defined."
exit -1
fi
# Optional environment variables
if [[ -z "$PCS_OFFICE365_CONNECTION_URL" ]]; then
echo "Warning: the PCS_OFFICE365_CONNECTION_URL environment variable is not defined."
fi
if [[ -z "$PCS_SOLUTION_NAME" ]]; then
echo "Warning: the PCS_SOLUTION_NAME environment variable is not defined."
fi
if [[ -z "$PCS_SUBSCRIPTION_ID" ]]; then
echo "Warning: the PCS_SUBSCRIPTION_ID environment variable is not defined."
fi
if [[ -z "$PCS_ARM_ENDPOINT_URL" ]]; then
echo "Warning: the PCS_ARM_ENDPOINT_URL environment variable is not defined."
fi

Просмотреть файл

@ -24,4 +24,22 @@ IF "%PCS_AUTH_WEBSERVICE_URL%" == "" (
echo Error: the PCS_AUTH_WEBSERVICE_URL environment variable is not defined.
exit /B 1
)
:: Optional environment variables
IF "%PCS_OFFICE365_CONNECTION_URL%" == "" (
echo Warning: the PCS_OFFICE365_CONNECTION_URL environment variable is not defined.
)
IF "%PCS_SOLUTION_NAME%" == "" (
echo Warning: the $PCS_SOLUTION_NAME environment variable is not defined.
)
IF "%PCS_SUBSCRIPTION_ID%" == "" (
echo Warning: the PCS_SUBSCRIPTION_ID environment variable is not defined.
)
IF "%PCS_ARM_ENDPOINT_URL%" == "" (
echo Warning: the PCS_ARM_ENDPOINT_URL environment variable is not defined.
)
endlocal

Просмотреть файл

@ -51,6 +51,10 @@ run_in_sandbox() {
-e PCS_DEVICESIMULATION_WEBSERVICE_URL \
-e PCS_TELEMETRY_WEBSERVICE_URL \
-e PCS_AUTH_WEBSERVICE_URL \
-e PCS_OFFICE365_CONNECTION_URL \
-e PCS_SOLUTION_NAME \
-e PCS_SUBSCRIPTION_ID \
-e PCS_ARM_ENDPOINT_URL \
-v "$PCS_CACHE/sandbox/.config:/root/.config" \
-v "$PCS_CACHE/sandbox/.dotnet:/root/.dotnet" \
-v "$PCS_CACHE/sandbox/.nuget:/root/.nuget" \

Просмотреть файл

@ -64,6 +64,10 @@ IF "%1"=="--in-sandbox" GOTO :RunInSandbox
-e PCS_DEVICESIMULATION_WEBSERVICE_URL ^
-e PCS_TELEMETRY_WEBSERVICE_URL ^
-e PCS_AUTH_WEBSERVICE_URL ^
-e PCS_OFFICE365_CONNECTION_URL ^
-e PCS_SOLUTION_NAME ^
-e PCS_SUBSCRIPTION_ID ^
-e PCS_ARM_ENDPOINT_URL ^
-v %PCS_CACHE%\sandbox\.config:/root/.config ^
-v %PCS_CACHE%\sandbox\.dotnet:/root/.dotnet ^
-v %PCS_CACHE%\sandbox\.nuget:/root/.nuget ^

Просмотреть файл

@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Http;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
using Moq;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Test
{
public class ActionManagerTest
{
private readonly ActionManager actionManager;
private readonly Mock<IHttpClient> httpClientMock;
public ActionManagerTest()
{
Mock<ILogger> loggerMock = new Mock<ILogger>();
this.httpClientMock = new Mock<IHttpClient>();
IServicesConfig servicesConfig = new ServicesConfig
{
LogicAppEndpointUrl = "https://azure.com",
SolutionUrl = "test",
TemplateFolder = ".\\data\\"
};
this.actionManager = new ActionManager(loggerMock.Object, servicesConfig, this.httpClientMock.Object);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task EmailAction_CausesPostToLogicApp()
{
// Arrange
JArray emailArray = new JArray(new object[] { "sampleEmail@gmail.com" });
Dictionary<string, object> actionParameters = new Dictionary<string, object>
{
{ "Recipients", emailArray },
{ "Notes", "Test Note" },
{ "Subject", "Test Subject" }
};
EmailAction testAction = new EmailAction(actionParameters);
AsaAlarmApiModel alarm = new AsaAlarmApiModel
{
DateCreated = 1539035437937,
DateModified = 1539035437937,
DeviceId = "Test Device Id",
MessageReceived = 1539035437937,
RuleDescription = "Test Rule description",
RuleId = "TestRuleId",
RuleSeverity = "Warning",
Actions = new List<IAction> { testAction }
};
var response = new HttpResponse(HttpStatusCode.OK, "", null);
this.httpClientMock.Setup(x => x.PostAsync(It.IsAny<IHttpRequest>())).ReturnsAsync(response);
List<AsaAlarmApiModel> alarmList = new List<AsaAlarmApiModel> { alarm };
// Act
await this.actionManager.ExecuteAlarmActions(alarmList);
// Assert
this.httpClientMock.Verify(x => x.PostAsync(It.IsAny<IHttpRequest>()));
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Test</RootNamespace>
<AssemblyName>Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Test</AssemblyName>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ActionsAgent\ActionsAgent.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Moq" Version="4.7.145" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Moq;
using Xunit;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Test
{
public class AlarmParserTest
{
private readonly Mock<ILogger> loggerMock;
public AlarmParserTest()
{
this.loggerMock = new Mock<ILogger>();
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void CanParse_ProperlyFormattedJson()
{
// Arrange
string data = "{\"created\":1539035437937,\"modified\":1539035437937,\"rule.description\":\"description\",\"rule.severity\":\"Warning\",\"rule.id\":\"TestRuleId\",\"rule.actions\":[{\"Type\":\"Email\",\"Parameters\":{\"Notes\":\"Test Note\",\"Subject\":\"Test Subject\",\"Recipients\":[\"sampleEmail@gmail.com\"]}}],\"device.id\":\"Test Device Id\",\"device.msg.received\":1539035437937}" +
"{\"created\":1539035437940,\"modified\":1539035437940,\"rule.description\":\"description2\",\"rule.severity\":\"Info\",\"rule.id\":\"1234\",\"device.id\":\"Device Id\",\"device.msg.received\":1539035437940}";
// Act
var result = AlarmParser.ParseAlarmList(data, this.loggerMock.Object);
// Assert
AsaAlarmApiModel[] resultArray = result.ToArray();
Assert.Equal(2, resultArray.Length);
Assert.Equal("description", resultArray[0].RuleDescription);
Assert.Equal("description2", resultArray[1].RuleDescription);
Assert.Equal(1, resultArray[0].Actions.Count);
var action = resultArray[0].Actions[0];
Assert.Equal(ActionType.Email, action.Type);
var recipients = ((EmailAction)action).GetRecipients();
Assert.Single(recipients);
Assert.Equal("sampleEmail@gmail.com", recipients[0]);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void LogsError_OnImproperlyFormattedJson()
{
// Arrange
// no commas
string data = "{\"created\":1539035437937\"modified\":1539035437937\"rule.description\":\"description\"\"rule.severity\":\"Warning\"}";
// Act
var result = AlarmParser.ParseAlarmList(data, this.loggerMock.Object);
// Assert
this.loggerMock.Verify(x => x.Error(It.IsAny<string>(), It.IsAny<Func<object>>()));
Assert.Empty(result);
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Test
{
/// <summary>
/// Use these flags to allow running a subset of tests from the test
/// explorer and the command line.
/// </summary>
public class Constants
{
public const string TYPE = "Type";
public const string UNIT_TEST = "UnitTest";
}
}

Просмотреть файл

@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Http;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions
{
public interface IActionManager
{
Task ExecuteAlarmActions(IEnumerable<AsaAlarmApiModel> alarms);
}
public class ActionManager : IActionManager
{
private readonly IActionExecutor emailActionExecutor;
public ActionManager(ILogger logger, IServicesConfig servicesConfig, IHttpClient httpClient)
{
this.emailActionExecutor = new EmailActionExecutor(
servicesConfig,
httpClient,
logger);
}
/**
* Given a string of alarms in format {AsaAlarmApiModel1}...{AsaAlarmApiModelN}
* For each alarm with an action, execute that action
*/
public async Task ExecuteAlarmActions(IEnumerable<AsaAlarmApiModel> alarms)
{
IEnumerable<AsaAlarmApiModel> alarmList = alarms.Where(x => x.Actions != null && x.Actions.Count > 0);
List<Task> actionList = new List<Task>();
foreach (var alarm in alarmList)
{
foreach (var action in alarm.Actions)
{
switch (action.Type)
{
case ActionType.Email:
actionList.Add(this.emailActionExecutor.Execute((EmailAction)action, alarm));
break;
}
}
}
await Task.WhenAll(actionList);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions
{
public static class AlarmParser
{
/// <summary>
/// Parse alarm list emitted by asa into event hub.
/// Alarms come in format:
/// {alarm1}{alarm2}...{alarmN}
/// Returns list of AsaAlarmApiModel objects
/// </summary>
public static IEnumerable<AsaAlarmApiModel> ParseAlarmList(string alarms, ILogger logger)
{
IList<AsaAlarmApiModel> alarmList = new List<AsaAlarmApiModel>();
JsonSerializer serializer = new JsonSerializer();
if (!String.IsNullOrEmpty(alarms))
{
using (JsonTextReader reader = new JsonTextReader(new StringReader(alarms)))
{
reader.SupportMultipleContent = true;
while (reader.Read())
{
try
{
alarmList.Add(serializer.Deserialize<AsaAlarmApiModel>(reader));
}
catch (Exception e)
{
logger.Error("Exception parsing the json string. Expected string in format {alarm}{alarm}...{alarm}",
() => new { e });
break;
}
}
}
}
return alarmList;
}
}
}

Просмотреть файл

@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Http;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions
{
public class EmailActionExecutor : IActionExecutor
{
private readonly IServicesConfig servicesConfig;
private readonly IHttpClient httpClient;
private readonly ILogger logger;
private const string EMAIL_TEMPLATE_FILE_NAME = "EmailTemplate.html";
private const string DATE_FORMAT_STRING = "r";
public EmailActionExecutor(
IServicesConfig servicesConfig,
IHttpClient httpClient,
ILogger logger)
{
this.servicesConfig = servicesConfig;
this.httpClient = httpClient;
this.logger = logger;
}
/// <summary>
/// Execute the given email action for the given alarm.
/// Sends a post request to Logic App with alarm information
/// </summary>
public async Task Execute(IAction action, object metadata)
{
if (metadata.GetType() != typeof(AsaAlarmApiModel)
|| action.GetType() != typeof(EmailAction))
{
string errorMessage = "Email action expects metadata to be alarm and action" +
" to be EmailAction, will not send email";
this.logger.Error(errorMessage, () => { });
return;
}
try
{
AsaAlarmApiModel alarm = (AsaAlarmApiModel)metadata;
EmailAction emailAction = (EmailAction)action;
string payload = this.GeneratePayload(emailAction, alarm);
HttpRequest httpRequest = new HttpRequest(this.servicesConfig.LogicAppEndpointUrl);
httpRequest.SetContent(payload);
IHttpResponse response = await this.httpClient.PostAsync(httpRequest);
if (!response.IsSuccess)
{
this.logger.Error("Could not execute email action against logic app", () => { });
}
}
catch (JsonException e)
{
this.logger.Error("Could not create email payload to send to logic app,", () => new { e });
}
catch (Exception e)
{
this.logger.Error("Could not execute email action against logic app", () => new { e });
}
}
/**
* Generate email payload for given alarm and email action.
* Creates subject, recipients, and body based on action and alarm
*/
private string GeneratePayload(EmailAction emailAction, AsaAlarmApiModel alarm)
{
string emailTemplate = File.ReadAllText(this.servicesConfig.TemplateFolder + EMAIL_TEMPLATE_FILE_NAME);
string alarmDate = DateTimeOffset.FromUnixTimeMilliseconds(alarm.DateCreated).ToString(DATE_FORMAT_STRING);
emailTemplate = emailTemplate.Replace("${subject}", emailAction.GetSubject());
emailTemplate = emailTemplate.Replace(
"${alarmDate}",
DateTimeOffset.FromUnixTimeMilliseconds(alarm.DateCreated).ToString(DATE_FORMAT_STRING));
emailTemplate = emailTemplate.Replace("${ruleId}", alarm.RuleId);
emailTemplate = emailTemplate.Replace("${ruleDescription}", alarm.RuleDescription);
emailTemplate = emailTemplate.Replace("${ruleSeverity}", alarm.RuleSeverity);
emailTemplate = emailTemplate.Replace("${deviceId}", alarm.DeviceId);
emailTemplate = emailTemplate.Replace("${notes}", emailAction.GetNotes());
emailTemplate = emailTemplate.Replace("${alarmUrl}", this.GenerateRuleDetailUrl(alarm.RuleId));
EmailActionPayload payload = new EmailActionPayload
{
Recipients = emailAction.GetRecipients(),
Subject = emailAction.GetSubject(),
Body = emailTemplate
};
return JsonConvert.SerializeObject(payload);
}
/**
* Generate URL to direct to maintenance dashboard for specific rule
*/
private string GenerateRuleDetailUrl(string ruleId)
{
return this.servicesConfig.SolutionUrl + "/maintenance/rule/" + ruleId;
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions
{
public interface IActionExecutor
{
Task Execute(IAction action, object metadata);
}
}

Просмотреть файл

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent</RootNamespace>
<AssemblyName>Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent</AssemblyName>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Services\Services.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.EventHubs" Version="2.2.0" />
<PackageReference Include="Microsoft.Azure.EventHubs.Processor" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<None Update="data\EmailTemplate.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs.Processor;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.EventHub;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent
{
public interface IAgent
{
Task RunAsync(CancellationToken runState);
}
public class Agent : IAgent
{
private readonly ILogger logger;
private readonly IServicesConfig servicesConfig;
private readonly IEventProcessorFactory actionsEventProcessorFactory;
private readonly IEventProcessorHostWrapper eventProcessorHostWrapper;
public Agent(ILogger logger,
IServicesConfig servicesConfig,
IEventProcessorHostWrapper eventProcessorHostWrapper,
IEventProcessorFactory actionsEventProcessorFactory)
{
this.logger = logger;
this.servicesConfig = servicesConfig;
this.actionsEventProcessorFactory = actionsEventProcessorFactory;
this.eventProcessorHostWrapper = eventProcessorHostWrapper;
}
public async Task RunAsync(CancellationToken runState)
{
this.logger.Info("Actions Agent started", () => { });
await this.SetupEventHub(runState);
}
private async Task SetupEventHub(CancellationToken runState)
{
if (!runState.IsCancellationRequested)
{
try
{
var eventProcessorHost = this.eventProcessorHostWrapper.CreateEventProcessorHost(
this.servicesConfig.ActionsEventHubName,
this.servicesConfig.ActionsEventHubConnectionString,
this.servicesConfig.BlobStorageConnectionString,
this.servicesConfig.ActionsBlobStorageContainer);
await this.eventProcessorHostWrapper.RegisterEventProcessorFactoryAsync(eventProcessorHost, this.actionsEventProcessorFactory);
}
catch (Exception e)
{
this.logger.Error("Received error setting up event hub. Will not receive information from the eventhub", () => new { e });
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.EventHubs.Processor;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.EventHub
{
public class ActionsEventProcessor : IEventProcessor
{
private readonly ILogger logger;
private readonly IActionManager actionManager;
public ActionsEventProcessor(IActionManager actionManager, ILogger logger)
{
this.logger = logger;
this.actionManager = actionManager;
}
public Task OpenAsync(PartitionContext context)
{
this.logger.Debug("Event Processor initialized.", () => new { context.PartitionId });
return Task.CompletedTask;
}
public async Task CloseAsync(PartitionContext context, CloseReason reason)
{
this.logger.Debug("Event Processor Shutting Down.", () => new { context.PartitionId, reason });
await context.CheckpointAsync();
}
/**
* Processes all alarms and executes any actions associated with the alarms
*/
public async Task ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> messages)
{
foreach (EventData eventData in messages)
{
if (eventData.Body.Array != null)
{
string data = Encoding.UTF8.GetString(eventData.Body.Array);
IEnumerable<AsaAlarmApiModel> alarms = AlarmParser.ParseAlarmList(data, this.logger);
await this.actionManager.ExecuteAlarmActions(alarms);
}
}
await context.CheckpointAsync();
}
public async Task ProcessErrorAsync(PartitionContext context, Exception error)
{
await context.CheckpointAsync();
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Azure.EventHubs.Processor;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Http;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.EventHub
{
public class ActionsEventProcessorFactory : IEventProcessorFactory
{
private readonly ILogger logger;
private readonly IActionManager actionManager;
public ActionsEventProcessorFactory(
ILogger logger,
IServicesConfig servicesConfig,
IHttpClient httpClient)
{
this.logger = logger;
this.actionManager = new ActionManager(logger, servicesConfig, httpClient);
}
public IEventProcessor CreateEventProcessor(PartitionContext context)
{
return new ActionsEventProcessor(this.actionManager, this.logger);
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.EventHubs.Processor;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.EventHub
{
public interface IEventProcessorHostWrapper
{
EventProcessorHost CreateEventProcessorHost(
string eventHubPath,
string eventHubConnectionString,
string storageConnectionString,
string leaseContainerName);
Task RegisterEventProcessorFactoryAsync(EventProcessorHost host, IEventProcessorFactory factory);
}
public class EventProcessorHostWrapper : IEventProcessorHostWrapper
{
public EventProcessorHost CreateEventProcessorHost(
string eventHubPath,
string eventHubConnectionString,
string storageConnectionString,
string leaseContainerName)
{
return new EventProcessorHost(
eventHubPath,
PartitionReceiver.DefaultConsumerGroupName,
eventHubConnectionString,
storageConnectionString,
leaseContainerName);
}
public Task RegisterEventProcessorFactoryAsync(EventProcessorHost host, IEventProcessorFactory factory)
{
return host.RegisterEventProcessorFactoryAsync(factory);
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models
{
public class AsaAlarmApiModel
{
[JsonProperty(PropertyName = "created")]
public long DateCreated { get; set; }
[JsonProperty(PropertyName = "modified")]
public long DateModified { get; set; }
[JsonProperty(PropertyName = "rule.description")]
public string RuleDescription { get; set; }
[JsonProperty(PropertyName = "rule.severity")]
public string RuleSeverity { get; set; }
[JsonProperty(PropertyName = "rule.id")]
public string RuleId { get; set; }
[JsonProperty(PropertyName = "rule.actions")]
public IList<IAction> Actions { get; set; }
[JsonProperty(PropertyName = "device.id")]
public string DeviceId { get; set; }
[JsonProperty(PropertyName = "device.msg.received")]
public long MessageReceived { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.ActionsAgent.Models
{
// Payload to send to logic app to generate an email alert
public class EmailActionPayload
{
[JsonProperty(PropertyName = "recipients")]
public List<string> Recipients { get; set; }
[JsonProperty(PropertyName = "body")]
public string Body { get; set; }
[JsonProperty(PropertyName = "subject")]
public string Subject { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,20 @@
<head>
<style>
.email-body {
font-family: "Segoe UI";
color: #3e4145;
}
</style>
</head>
<body class="email-body">
<h1>${subject}</h1>
<h2>Details</h2>
<p><b>Time Triggered:</b> ${alarmDate}</p>
<p><b>Rule Id:</b> ${ruleId}</p>
<p><b>Rule Description:</b> ${ruleDescription}</p>
<p><b>Severity:</b> ${ruleSeverity}</p>
<p><b>Device Id:</b> ${deviceId}</p>
<h2>Notes</h2>
<p>${notes}</p>
<p>See alert and device details <a href="${alarmUrl}">here</a></p>
</body>

Просмотреть файл

@ -63,9 +63,17 @@ for more information. More information on environment variables
* see: Azure Portal => Azure Active Directory => App Registrations => Your App => Settings => Passwords
* `PCS_TELEMETRY_STORAGE_TYPE` = "tsi"
* Allowed values: ["cosmosdb", "tsi"]. Default is "tsi"
* `PCS_TSI_FQDN`= {Time Series FQDN}
* `PCS_TSI_FQDN` = {Time Series FQDN}
* see: Azure Portal => Your Resource Group => Time Series Insights Environment => Data Access FQDN
* `PCS_DIAGNOSTICS_WEBSERVICE_URL` (optional) = http://localhost:9006/v1
* `PCS_ACTION_EVENTHUB_NAME` = {Event hub name}
* `PCS_ACTION_EVENTHUB_CONNSTRING` = {Endpoint=sb://....servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...}
* see: Azure Portal => Your resource group => your event hub namespace => Shared access policies
* `PCS_LOGICAPP_ENDPOINT_URL` = {Logic App Endpoint}
* see: Azure Portal => Your resource group => Your Logic App => Logic App Designer => When a Http Request is received => HTTP POST URL
* `PCS_AZUREBLOB_CONNSTRING` = {connection string}
* see: Azure Portal => Your resource group => Your Storage Account => Access keys => Connection String
* `PCS_SOLUTION_WEBSITE_URL` = {Solution Url}
## Running the service with Visual Studio or VS Code
@ -83,6 +91,14 @@ for more information. More information on environment variables
1. `PCS_STORAGEADAPTER_WEBSERVICE_URL` = http://localhost:9022/v1
1. `PCS_AUTH_WEBSERVICE_URL` = http://localhost:9001/v1
1. `PCS_DIAGNOSTICS_WEBSERVICE_URL` (optional) = http://localhost:9006/v1
1. `PCS_TELEMETRY_STORAGE_TYPE` = "tsi"
1. `PCS_TSI_FQDN` = {Time Series FQDN}
1. `PCS_DIAGNOSTICS_WEBSERVICE_URL` (optional) = http://localhost:9006/v1
1. `PCS_ACTION_EVENTHUB_NAME` = {Event hub name}
1. `PCS_ACTION_EVENTHUB_CONNSTRING` = {Endpoint=sb://....servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...}
1. `PCS_LOGICAPP_ENDPOINT_URL` = {Logic App Endpoint}
1. `PCS_AZUREBLOB_CONNSTRING` = {connection string}
1. `PCS_SOLUTION_WEBSITE_URL` = {Solution Url}
1. Start the WebService project (e.g. press F5).
1. Using an HTTP client like [Postman][postman-url], use the
[RESTful API][project-wiki] to test out the service.
@ -97,6 +113,14 @@ More information on environment variables
1. `PCS_STORAGEADAPTER_WEBSERVICE_URL` = http://localhost:9022/v1
1. `PCS_AUTH_WEBSERVICE_URL` = http://localhost:9001/v1
1. `PCS_DIAGNOSTICS_WEBSERVICE_URL` (optional) = http://localhost:9006/v1
1. `PCS_TELEMETRY_STORAGE_TYPE` = "tsi"
1. `PCS_TSI_FQDN` = {Time Series FQDN}
1. `PCS_DIAGNOSTICS_WEBSERVICE_URL` (optional) = http://localhost:9006/v1
1. `PCS_ACTION_EVENTHUB_NAME` = {Event hub name}
1. `PCS_ACTION_EVENTHUB_CONNSTRING` = {Endpoint=sb://....servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...}
1. `PCS_LOGICAPP_ENDPOINT_URL` = {Logic App Endpoint}
1. `PCS_AZUREBLOB_CONNSTRING` = {connection string}
1. `PCS_SOLUTION_WEBSITE_URL` = {Solution Url}
1. Use the scripts in the [scripts](scripts) folder for many frequent tasks:
* `build`: compile all the projects and run the tests.
* `compile`: compile all the projects.

Просмотреть файл

@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Services.Test.helpers;
using Xunit;
namespace Services.Test
{
public class ActionConverterTest
{
private const string PARAM_NOTES = "Chiller pressure is at 250 which is high";
private const string PARAM_SUBJECT = "Alert Notification";
private const string PARAM_RECIPIENTS = "sampleEmail@gmail.com";
private const string PARAM_NOTES_KEY = "Notes";
private const string PARAM_RECIPIENTS_KEY = "Recipients";
public ActionConverterTest() { }
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ItReturnsEmailAction_WhenEmailActionJsonPassed()
{
// Arrange
const string SAMPLE_JSON = "[{\"Type\":\"Email\"," +
"\"Parameters\":{\"Notes\":\"" + PARAM_NOTES +
"\",\"Subject\":\"" + PARAM_SUBJECT +
"\",\"Recipients\":[\"" + PARAM_RECIPIENTS + "\"]}}]";
// Act
var rulesList = JsonConvert.DeserializeObject<List<IAction>>(SAMPLE_JSON);
// Assert
Assert.NotEmpty(rulesList);
Assert.Equal(ActionType.Email, rulesList[0].Type);
Assert.Equal(PARAM_NOTES, rulesList[0].Parameters[PARAM_NOTES_KEY]);
Assert.Equal(new JArray { PARAM_RECIPIENTS }, rulesList[0].Parameters[PARAM_RECIPIENTS_KEY]);
}
}
}

Просмотреть файл

@ -0,0 +1,132 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Newtonsoft.Json.Linq;
using Services.Test.helpers;
using Xunit;
namespace Services.Test
{
public class ActionTest
{
private const string PARAM_NOTES = "Chiller pressure is at 250 which is high";
private const string PARAM_SUBJECT = "Alert Notification";
private const string PARAM_RECIPIENTS = "sampleEmail@gmail.com";
private const string PARAM_SUBJECT_KEY = "Subject";
private const string PARAM_NOTES_KEY = "Notes";
private const string PARAM_RECIPIENTS_KEY = "Recipients";
private readonly JArray emailArray;
public ActionTest()
{
this.emailArray = new JArray { PARAM_RECIPIENTS };
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_ReturnActionModel_When_ValidActionType()
{
// Arrange
var parameters = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ PARAM_SUBJECT_KEY, PARAM_SUBJECT },
{ PARAM_NOTES_KEY, PARAM_NOTES },
{ PARAM_RECIPIENTS_KEY, this.emailArray }
};
// Act
var result = new EmailAction(parameters);
// Assert
Assert.Equal(ActionType.Email, result.Type);
Assert.Equal(PARAM_NOTES, result.Parameters[PARAM_NOTES_KEY]);
Assert.Equal(this.emailArray, result.Parameters[PARAM_RECIPIENTS_KEY]);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_ThrowInvalidInputException_When_ActionTypeIsEmailAndInvalidEmail()
{
// Arrange
var parameters = new Dictionary<string, object>()
{
{ PARAM_SUBJECT_KEY, PARAM_SUBJECT },
{ PARAM_NOTES_KEY, PARAM_NOTES },
{ PARAM_RECIPIENTS_KEY, new JArray() { "sampleEmailgmail.com"} }
};
// Act and Assert
Assert.Throws<InvalidInputException>(() => new EmailAction(parameters));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_Throw_InvalidInputException_WhenActionTypeIsEmailAndNoRecipients()
{
// Arrange
var parameters = new Dictionary<string, object>()
{
{ PARAM_SUBJECT_KEY, PARAM_SUBJECT },
{ PARAM_NOTES_KEY, PARAM_NOTES }
};
// Act and Assert
Assert.Throws<InvalidInputException>(() => new EmailAction(parameters));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_ThrowInvalidInputException_When_ActionTypeIsEmailAndEmailIsString()
{
// Arrange
var parameters = new Dictionary<string, object>()
{
{ PARAM_SUBJECT_KEY, PARAM_SUBJECT },
{ PARAM_NOTES_KEY, PARAM_NOTES },
{ PARAM_RECIPIENTS_KEY, PARAM_RECIPIENTS }
};
// Act and Assert
Assert.Throws<InvalidInputException>(() => new EmailAction(parameters));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_ReturnActionModel_When_ValidActionTypeParametersIsCaseInsensitive()
{
// Arrange
var parameters = new Dictionary<string, object>()
{
{ "subject", PARAM_SUBJECT },
{ "nOtEs", PARAM_NOTES },
{ "rEcipiEnts", this.emailArray }
};
// Act
var result = new EmailAction(parameters);
// Assert
Assert.Equal(ActionType.Email, result.Type);
Assert.Equal(PARAM_NOTES, result.Parameters[PARAM_NOTES_KEY]);
Assert.Equal(this.emailArray, result.Parameters[PARAM_RECIPIENTS_KEY]);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void Should_CreateAction_When_OptionalNotesAreMissing()
{
// Arrange
var parameters = new Dictionary<string, object>()
{
{ PARAM_SUBJECT_KEY, PARAM_SUBJECT },
{ PARAM_RECIPIENTS_KEY, this.emailArray }
};
// Act
var result = new EmailAction(parameters);
// Assert
Assert.Equal(ActionType.Email, result.Type);
Assert.Equal(string.Empty, result.Parameters[PARAM_NOTES_KEY]);
Assert.Equal(this.emailArray, result.Parameters[PARAM_RECIPIENTS_KEY]);
}
}
}

Просмотреть файл

@ -10,6 +10,7 @@ using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.External;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Http;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Runtime;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Storage.StorageAdapter;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.StorageAdapter;
@ -17,6 +18,7 @@ using Moq;
using Newtonsoft.Json;
using Services.Test.helpers;
using Xunit;
using Type = System.Type;
namespace Services.Test
{
@ -60,7 +62,7 @@ namespace Services.Test
var list = await this.rulesMock.Object.GetListAsync(null, 0, LIMIT, null, false);
// Assert
Assert.Equal(0, list.Count);
Assert.Empty(list);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
@ -74,6 +76,11 @@ namespace Services.Test
// Assert
Assert.NotEmpty(list);
foreach (Rule rule in list)
{
Assert.NotNull(rule.Actions);
}
}
/**
@ -416,6 +423,17 @@ namespace Services.Test
}
};
var sampleActions = new List<IAction>
{
new EmailAction(
new Dictionary<string, object>
{
{ "recipients", new Newtonsoft.Json.Linq.JArray(){ "sampleEmail@gmail.com", "sampleEmail2@gmail.com" } },
{ "subject", "Test Email" },
{ "notes", "Test Email Notes." }
})
};
var sampleRules = new List<Rule>
{
new Rule()
@ -425,7 +443,8 @@ namespace Services.Test
Description = "Sample description 1",
GroupId = "Prototyping devices",
Severity = SeverityType.Critical,
Conditions = sampleConditions
Conditions = sampleConditions,
Actions = sampleActions
},
new Rule()
{
@ -434,7 +453,8 @@ namespace Services.Test
Description = "Sample description 2",
GroupId = "Prototyping devices",
Severity = SeverityType.Warning,
Conditions = sampleConditions
Conditions = sampleConditions,
Actions = sampleActions
}
};

Просмотреть файл

@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Converters
{
public class ActionConverter : JsonConverter
{
private const string ACTION_TYPE_KEY = "Type";
private const string PARAMETERS_KEY = "Parameters";
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IAction);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue,
JsonSerializer serializer)
{
JObject jsonObject = JObject.Load(reader);
var actionType = Enum.Parse(
typeof(ActionType),
jsonObject.GetValue(ACTION_TYPE_KEY).Value<string>(),
true);
var parameters = jsonObject.GetValue(PARAMETERS_KEY).ToString();
switch (actionType)
{
case ActionType.Email:
Dictionary<string, object> emailParameters =
JsonConvert.DeserializeObject<Dictionary<string, object>>(
parameters,
new EmailParametersConverter());
return new EmailAction(emailParameters);
}
// If could not deserialize, throw exception
throw new InvalidInputException($"Could not deseriailize action with type {actionType}");
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException("Use default implementation for writing to the field.");
}
}
}

Просмотреть файл

@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Converters
{
public class EmailParametersConverter : JsonConverter
{
private const string RECIPIENTS_KEY = "Recipients";
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IDictionary<string, object>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jsonObject = JObject.Load(reader);
// Convert to a case-insensitive dictionary for case insensitive look up.
Dictionary<string, object> result =
new Dictionary<string, object>(jsonObject.ToObject<Dictionary<string, object>>(), StringComparer.OrdinalIgnoreCase);
if (result.ContainsKey(RECIPIENTS_KEY) && result[RECIPIENTS_KEY] != null)
{
result[RECIPIENTS_KEY] = ((JArray)result[RECIPIENTS_KEY]).ToObject<List<string>>();
}
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Use default implementation for writing to the field.");
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.DeviceTelemetry.Services.Models.Actions
{
/// <summary>
/// Interface for all Actions that can be added as part of a Rule.
/// New action types should implement IAction and be added to the ActionType enum.
/// Parameters should be a case-insensitive dictionary used to pass additional
/// information required for any given action type.
/// </summary>
[JsonConverter(typeof(ActionConverter))]
public interface IAction
{
[JsonConverter(typeof(StringEnumConverter))]
ActionType Type { get; }
// Note: Parameters should always be initialized as a case-insensitive dictionary
IDictionary<string, object> Parameters { get; }
}
public enum ActionType
{
Email
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше