Leveraging Logic Apps to prevent over-provisioning owner access to subscriptions
Tags: azure, logicapps, security, cloudnative
Published: 2021-11-11 08:06:51

Often happens that agility and freedom conflict with security.

#### (Aaron Paul voice) Has this ever happened to you?

Have you ever had developer teams request ownership of a full **subscription** to be able to freely experiment?

You still want to keep isolation, segregate responsibilities and permissions.

Ability to experiment freely is paramount to innovation, but uncontrolled proliferation of **subscriptions** can bear a significant management overhead.

Can we have the best of both worlds? The short answer is: yes.

A **resource group** can be an effective boundary as it allows its [contributors](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#contributor) to yet create any resource, but also restrict the scope of access within a **subscription**.

You can also enforce **tags** and **Azure policies** to control costs and enforce security.

But now a team is restricted to creating resources within a single **resource group** and it can get messy quite quickly and permission-wise is not so granular within teams.

What if we could allow teams to create their own **resource groups** within a subscription with **contributor** access and not being able to read/write other **resource groups**?

We can quickly set up a **Logic App** to enable this. Orchestrating creation and role assignment of **resource groups** within a single workflow, this enables:

* Invoking the **Logic App** manually through a **REST API**
* Invoking from a **DevOps** pipeline to create resources as part of a dev/test automated environment

The **Logic App** can create a **resource group** and assign a certain **contributor** based on the input payload from the **HTTP trigger**.

**Warning**

Setting up this workflow could lead to a security loophole: What if someone uses the name of an existing **resource group** so the workflow grants access to other teams' resources? We need to make sure we address this concern when building our **Logic App** as, usually, **Azure management** operations are idempotent and the **Logic App** won't fail if we pass the name of an existing **resource group**.

### The Logic App

Let's have a look at the flow:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uwdlqd640foct4sczm9t.png)

The logic is pretty simple and most of the operations we require have a native **connector**; the only missing one is "Create role assignment", but we can easily perform the operation by invoking the **Azure REST API** and we do not have to worry about authentication as the **managed identity** will do this for us.

Now, you may have noticed that there is no condition stating: "If the group already exists, interrupt the flow"; but if you look at the picture above, you may notice a red dotted line between two operations, this is because we changed the `Run after` settings of our create operation:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vfdhcsqm4yriknx97a8r.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/st0paar2hit118lzziex.png)

As you can see, the operation of creation (and so the role assignment after) will only occur if the `Read resource group` failed, hence the group does not exist; this will block the loophole described above.

Now, we can prevent people from having broad access, but our **Logic App's managed identity** still requires **owner** permissions on the **subscription**; more secure, but we can do even better. Let's create a **custom role** that has only enough permissions to read/create a **resource group** and assign permissions to it:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fyeboby4irz9kfksngc.png)

We can now use the UI to create a **custom role**, but we may also want to script it and define it in JSON format:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8uawl707bx43x38m6728.png)

Let's now assign that to the **managed identity** of our **Logic App**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qa5aslbmf8h4fedpdtlt.png)

Now we should have all permissions in place. If you also want to use a **security group**, your **Logic App** identity may also require `Directory.Read.All` permissions on your **Azure AD** instance.

### Creating role assignment

I mentioned above that all the other actions can be performed with native **Logic App connectors**, but the **role assignment**, at the time of writing, requires the **HTTP** connector to invoke the **Azure REST API**, let's have a look at that:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ntr7n35qcj9uv9oe65ki.png)

Even if we cannot do it in idiomatic **Logic App**, that is yet pretty simple.

This is the **API** documentation: https://docs.microsoft.com/en-us/rest/api/authorization/role-assignments/create

We just need to set the right method (`PUT`), the correct **URL**, using the **resource group ID** as scope from the output of the previous **connector** and we can auto-generate a random **GUID** as name for the assignment using **Logic App** expressions (`guid()`). The body must contain the `role definition ID`, which needs to be the built-in `contributor` **GUID** under our **subscription ID**, I have used a variable for that to improve clarity:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z827fnwr7rq3c2cel1xh.png)

The **subscription ID** is one of the input of our workflow and the hardcoded **GUID** can be found here: [Contributor](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#contributor)

To get values as input from the workflow invocation, we need to set the input **JSON schema** in the **HTTP trigger**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y561ryxxpqdaxcepd2b8.png)

We need:


* `principalId` The object ID of the assignee of the **contributor** role, this can be found looking up the user in **Azure AD** from the portal
* `resourceGroupLocation`, `resourceGroupName`, `subscriptionId` Quite self-explanatory arguments

```json
{
    "properties": {
        "principalId": {
            "type": "string"
        },
        "resourceGroupLocation": {
            "type": "string"
        },
        "resourceGroupName": {
            "type": "string"
        },
        "subscriptionId": {
            "type": "string"
        }
    },
    "type": "object"
}
```

After adding the schema above to the trigger, those values will be available as variables in the rest of the workflow.

### Testing

All there is left now is to test, let's run our workflow with the following input:

```json
{
    "subscriptionId": "<Target subscription ID to create groups>",
    "resourceGroupName": "rg-test2",
    "resourceGroupLocation": "West Europe",
    "principalId": "<Object ID GUID of your user from AAD>"
}
```

And this is what happens, assuming `rg-test2` does not exist:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qr20p3alwm0dfu1o94c.png)

Looks good, all the steps we wanted to run were successful.

Now, let's try and run this again with the same inputs:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tcqqsn78yxylzomr6y16.png)

OK, as you can see from the grey circles next to the actions below `Read a resource group`, none of the other operations were performed, exactly as expected.

Now let's have a look at our newly created **resource group IAM blade**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4blcaco3h5f91gdrqitf.png)

Exactly what we wanted; a new **resource group** of which I am **contributor** without requiring any permission on the **subscription**.

The full template for the **Logic App** is available on my [GitHub](https://github.com/UnoSD/ResourceGroupsLogicApp) so you can save the extra 5 minutes it took me to create it to enable this security feature for your teams.

You can also enhance security of the **Logic App** to prevent unauthorised users from calling it by fronting it with **API Management** or you can use **Azure Active Directory Authorization Policies** on the **Logic App** itself or a combination of the two.