首页 > 其他分享 >Dynamics 365 F&O and firewalls - monitor Azure IP ranges

Dynamics 365 F&O and firewalls - monitor Azure IP ranges

时间:2024-04-25 17:26:33浏览次数:28  
标签:function will monitor IP ranges file Azure new addresses

Contents  hide 

1 Azure IP Ranges: can we monitor them? 2 My proposal: an Azure function 2.1 Authentication 2.2 The function 2.3 Environment variables 2.4 HTTP call 2.5 Function response 3 Using the function 4 Let’s test it! 4.1 Subscribe!

If you’re integrating Dynamics 365 Finance & Operations with 3rd parties, and your organization or the 3rd party one are using a firewall, you might’ve found yourself in the scenario of being asked “which is the production/sandbox IP address?”.

Well, we don’t know. We know which IP it has now, but we don’t know if it will have the same IP in the future, you will have to monitor this if you plan on opening single IPs. This is something Dag Calafell wrote about on his blog: Static IP not guaranteed for Dynamics 365 for Finance and Operations.

I monitor with my eyeI monitor with my eye

So, what should I do if I have a firewall and need to allow access to/from Dynamics 365 F&O or any other Azure service? The network team usually doesn’t like the answer: if you can’t allow a FQDN, you should open all the address ranges for the datacenter and service you want to access. And that’s a lot of addresses that make the network team sad.

In today’s post, I’ll show you a way to keep an eye on the ranges provided by Microsoft, and hopefully make our life easier.

WARNING: due to this LinkedIn comment, I want to remark that the ranges you can find using this method are for INBOUND communication into Dynamics 365 or whatever service. For outbound communication, check this on Learn: For my Microsoft-managed environments, I have external components that have dependencies on an explicit outbound IP safe list. How can I ensure my service is not impacted after the move to self-service deployment?

Azure IP Ranges: can we monitor them?

Microsoft offers a JSON file you can download with the ranges for all its public cloud datacenters and different services. Yes, a file, not an API.

But wait, don’t complain yet, there IS an API we can use: the Azure REST API. And specifically the Service Tags section under Virtual Networks. The results from calling this API or downloading the file are a bit different, the JSON is structured differently, but both could serve our purpose.

My proposal: an Azure function

We will be querying a REST API, so we could perfectly be using a Power Automate flow to do this. Why am I overdoing things with an Azure function? Because I hate parsing JSON in Power Automate. That’s the main reason, but not the only one. Also because I love Azure functions.

Authentication

To authenticate and be able to access the Azure REST API we need to create an Azure Active Directory app registration, we’ve done this a million times, right? No need to repeat it.

We will need a secret for that app registration too. Keep both. We will create a service principal using it.

Go to your subscription, to the “Access control (IAM)” section and add a new role assignment:

Create a service principalCreate a service principal

Select the contributor role and click next:

Contributor roleContributor role

Click the select members text and look for the name of the app registration you created before:

Add membersAdd members

Finally, click the “Select” button, and the “Review + assign” one to end. Now we have a service principal with access to a subscription. We will use this to authenticate and access the Azure REST API.

The function

The function will have an HTTP trigger, and will read and write data in an Azure Table. We will do a POST call to the function’s endpoint, and that will trigger the process.

Nice diagramNice diagram

Then the function will download the JSON content from the Azure REST API, do several things, like looking into the table to see if we stored a previous version of the addresses file, compare both and save the latest version into the table. This is the code of the function:

[FunctionName("CheckRanges")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string ret = string.Empty;

            try
            {
                string body = String.Empty;

                using (StreamReader streamReader = new StreamReader(req.Body))
                {
                    body = await streamReader.ReadToEndAsync();
                }

                if (string.IsNullOrEmpty(body))
                {
                    throw new Exception("No request body found.");
                }

                dynamic data = JsonConvert.DeserializeObject(body);
                string serviceTagRegion = data.serviceTagRegion;
                string region = data.region;

                if (string.IsNullOrEmpty(serviceTagRegion) || string.IsNullOrEmpty(region))
                {
                    throw new Exception("The values in the cannot be empty.");
                }

                // Get token and call the API
                var token = GetToken().Result;

                var latestServiceTag = GetFile(token, region).Result;

                if (latestServiceTag is null)
                {
                    throw new Exception("No tag file has been downloaded.");
                }

                // Download existing file from the blob, if exists, and compare the root changeNumber                
                var existingServiceTagEntity = await ReadTableAsync();

                // If there's a file in the blob container we retrieve it and compare the changeNumber value. If it's the same there's no changes in the file.
                if (existingServiceTagEntity is not null)
                {
                    if (existingServiceTagEntity.ChangeNumber == latestServiceTag.changeNumber)
                    {
                        // Return empty containers in the JSON file
                        AddressChanges diff = new AddressChanges();

                        diff.addedAddresses = Array.Empty<string>();
                        diff.removedAddresses = Array.Empty<string>(); ;

                        ret = JsonConvert.SerializeObject(diff);

                        log.LogInformation("The downloaded file has the same changenumber as the already existing one. No changes.");

                        // Return empty JSON containers
                        return new OkObjectResult(ret);
                    }
                }

                // Process the new file
                var serviceTagSelected = latestServiceTag.values.FirstOrDefault(st => st.name.ToLower() == serviceTagRegion);

                if (serviceTagSelected is not null)
                {
                    ServiceTagAddresses addresses = new ServiceTagAddresses();

                    addresses.rootchangenumber = latestServiceTag.changeNumber;
                    addresses.nodename = serviceTagSelected.name;
                    addresses.nodechangenumber = serviceTagSelected.properties.changeNumber;
                    addresses.addresses = serviceTagSelected.properties.addressPrefixes;

                    // If a file exists in the table get the differences
                    if (existingServiceTagEntity is not null)
                    {
                        string[] existingAddresses = JsonConvert.DeserializeObject<string[]>(existingServiceTagEntity.Addresses);

                        ret = CompareAddresses(existingAddresses, addresses.addresses);
                    }

                    // Finally upload the file with the new addresses
                    var newAddressJson = JsonConvert.SerializeObject(addresses);

                    //await UploadFileAsync(fileName, newAddressJson);
                    await WriteToTableAsync(addresses);
                }
                else
                {
                    AddressChanges diff = new AddressChanges();

                    diff.addedAddresses = Array.Empty<string>();
                    diff.removedAddresses = Array.Empty<string>();

                    ret = JsonConvert.SerializeObject(diff);

                    // Return empty JSON containers
                    return new OkObjectResult(ret);
                }
            }
            catch (Exception ex)
            {
                return new BadRequestObjectResult(ex.Message);
            }

            return new OkObjectResult(ret);
        }

I’ve added comments to the code to make it clearer, but we’ll go a bit through it. You can find the complete Visual Studio solution in the AzureTagsIPWatcher GitHub repo.

Environment variables

You will find this piece of code several times in the code. This is used to retrieve the environment variables:

Environment.GetEnvironmentVariable("ContainerName")

It means that once you’ve deployed the function, you need to create as many different application settings in the Azure function as different variables you’ll find in the code. The variables are used to save the value of keys, secrets, etc. to create the connections to storage and get the token credentials. These are the ones I have used, and you should complete with your own values:

  • ContainerConn: the connection string to your Azure storage account.
  • ContainerName: the name of your storage account.
  • StorageKey: the key for your storage account.
  • StorageTable: the name of the table.
  • appId: your app registration id.
  • secret: the secret in your app registration.
  • tokenUrl: this will be the URL used to get the token. You need to change YOUR_TENANT_ID by the id of your tenant: “https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/token”.
  • resource: “https://management.azure.com/”, this is the root URL we will authenticate to.
  • apiUrl: “https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/providers/Microsoft.Network/locations/{0}/serviceTags?api-version=2022-07-01”, and this is the API endpoint URL. You need to change YOUR_SUBSCRIPTION_ID for the id of the subscription in which you’ve created the service principal.

HTTP call

The function is expecting a POST call with a JSON body in its request, containing the node we want to get the addresses from, and also the region:

{
    "serviceTagRegion": "azurecloud.westeurope",
    "region": "westeurope"
}

Function response

The function will return a JSON file with two containers, one for added addresses if any, and one for removed addresses:

{
    "removedAddresses":[],
    "addedAddresses":[]
}

Using the function

Now we have an Azure function with an HTTP trigger that returns a JSON string with two nodes with the changed addresses, or empty nodes if no changes exist.

How can we trigger this function on a schedule? With Power Automate, of course! We can use a scheduled cloud flow, that runs once a day, or as many times as you want.

Once we have a trigger, we will call the function. I like to create “Compose” blocks where I “initialize” the value of several “variables”. They’re not variables, but you’re getting what I mean, right?

In this first compose block I just add the body of the function:

Function bodyFunction body

Then we add an HTTP action, we set it to the POST method and add the function URL, and in the body the output of the previous compose block:

Calling the Azure functionCalling the Azure function

The next step will be parsing the response of the function. It will always return the containers, filled in or empty, so you can use this as the payload to generate the schema. And finally add the Body output of the function:

Parsing the function outputParsing the function output

Then I will create two more “Compose” blocks where I’ll save the output values of parsing the JSON, one for the added addresses and one for the removed ones:

Values from the JSON parsingValues from the JSON parsing

And now I add a condition where we’ll check whether there’s anything inside the new or deleted arrays. How can we check this? We will use an expression in the value field to check if the container’s length is 0:

Check if the arrays are emptyCheck if the arrays are empty

And if it’s 0 (“If yes” branch) we’ll do nothing, and if it contains elements (“If no” branch) we’ll send an email:

OutcomesOutcomes

Let’s test it!

To test it, I’ll go into my Azure Table and edit the latest entry it has, because I’ll run the function locally several times. I’ll change the value of the columns ChangeNumber and Addresses.

Changing valuesChanging values

And now run the flow…

Flow runFlow run

We can see the flow has gone through the “If no” branch in the condition, because the arrays contained changes. And I can see those in the email I’ve received:

Email from FlowEmail from Flow

And that’s all! As I said at the beginning, this could also be done solely using a Power automate cloud flow, but I preferred using an Azure function to do the dirty job of comparing values.

标签:function,will,monitor,IP,ranges,file,Azure,new,addresses
From: https://www.cnblogs.com/lingdanglfw/p/18158164

相关文章

  • JavaScript基本语法
    JavaScript基本语法变量变量是用于存放数据的容器,我们通过变量名获取数据,甚至数据可以修改。变量本质是程序在内存中申请的一块用来存放数据的空间。变量的使用变量在使用时分为两步:1.声明变量2.赋值变量的声明varname;声明一个变量,没有初始化var是一个JS关键字,用......
  • JavaScript简介
    JavaScript简介JavaScript是什么JavaScipt是运行在客户端(浏览器)上的一种编程语言(脚本语言)注:脚本语言:不需要编译,运行过程中由js解释器逐行解释并且执行现在也可以基于node.js技术进行服务器编程浏览器分成两部分∶渲染引擎和JS引擎渲染引擎︰用来解析HTML与CSS,俗称内核,比......
  • JavaScript对象
    JavaScript对象一、数组数组(Array)是指一组数据的集合,其中的每个数据被称作元素,数组是属于内置对象,数组和普通对象的功能类似,都可以用来存储一些值。不同的是:普通对象是使用字符串作为属性名,而数组是使用数字作为索引来操作元素。索引:从0开始的整数就是索引。在数组中......
  • JavaScript元素
    JavaScript元素一.常量常量也称之为“字面量”,是固定值,不可改变。看见什么,它就是什么。常量有下面这几种:数字常量(数值常量)字符串常量布尔常量自定义常量1.1数字常量数字常量非常简单,直接写数字就行,不需要任何其他的符号。既可以是整数,也可以是浮点数。consol......
  • JavaScript基本数据类型
    JavaScript基本数据类型上述一章我们讲到JS数据类型分为基本数据类型与引用数据类型,这张我们主要讲基本数据类型基本数据类型-String字符串字符串是我们开发中经常使用的一种数据类型,主要是双引号""或者单引号''注意事项单引号与双引号不能混用,必须配对使用同类引......
  • 开源相机管理库Aravis例程学习(四)——multiple-acquisition-signal
    目录简介例程代码函数说明g_main_loop_newg_main_loop_rung_main_loop_quitg_signal_connectarv_stream_set_emit_signalsQ&A回调函数的同步调用与异步调用帧丢失问题简介本文针对官方例程中的:02-multiple-acquisition-signal做简单的讲解。并简单介绍其中调用的g_main_loop_new......
  • Javascript的数据类型和json数组
    4个数据类型:NumberStringBooleanUndefinedalert(parseInt(k));//如果不是数字会输出NaN,从第一个字符开始输出数字,直到不是数字后返回值。json数组://js中k、v型数据,使用jsonvarperson={name:"张三",//注意里面的元素用,分割。定义的是key是name的value值为张三age......
  • Linux:VMware切换"仅主机模式"并配置静态IP
    配置网络编辑器点击“编辑”->“虚拟网络编辑器”没有仅主机模式的话,可以通过“添加网络”进行新增网络配置。更改虚拟机网路模式右键“创建的虚拟就”->“设置”登录虚拟机配置静态IP切换目录到“/etc/sysconfig/network-scripts/”修改“if-ens33”文件TYPE=Ethern......
  • javaScript for-in循环
    for-infor-in循环是专门为循环对象设置的,因为对象没有长度没有顺序,所以不能使用for循环。for-in循环可以循环数组和对象,推荐循环对象的时候使用constobj={name:"LiuQing",age:18,sex:'男'}for(constkeyinobj){consol......
  • javascript 对象方法、实例方法
    在JavaScript中,对象方法和实例方法通常指的是类(构造函数)中的方法。然而,JavaScript并没有像一些其他面向对象编程语言(如Java或C++)那样的类关键字。相反,JavaScript使用构造函数和原型来模拟类的行为。实例方法:实例方法是定义在构造函数原型上的方法,它们可以通过构造函数的实例来调......