Merge branch 'adm_master' of https://github.com/Azure/remote-monitoring-services-dotnet into adm_master
This commit is contained in:
Коммит
b5a7ac4f3d
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче