Open boillodmanuel opened 3 years ago
So, we are talking about this API endpoint which has a fixed path /providers/Microsoft.Management/managementGroups/{groupId}
irrespective of the management group's parent.
Before creating a group, we fire a GET request to see if the endpoint is available, which causes your error.
Do you know what is wrong here? If you create a group at "managementGroup" level - is its URL still the same? Will you have access to it at that URL?
I think I got it.
Using az cli from my user with rights at the root management group level
/providers/Microsoft.Management/managementGroups/mg-test
) causes a permission error (because my user has no rights at the tenant scope /
)mg-test
) worksFull details :
$ az account management-group show --name /providers/Microsoft.Management/managementGroups/mg-test
code: AuthorizationFailed - , The client 'HIDDEN' with object id 'HIDDEN' does not have authorization to perform action 'Microsoft.Management/managementGroups/Microsoft.Management/mg-test/read' over scope '/providers/Microsoft.Management/managementGroups/providers/Microsoft.Management/managementGroups' or the scope is invalid. If access was recently granted, please refresh your credentials.
$ az account management-group show --name mg-test
{
"id": "/providers/Microsoft.Management/managementGroups/mg-test",
"name": "mg-test",
...
}
The former (using the full resource id) works only if you have permission at the tenant level (scope /
) (I didn't test with az-cli, but I'm pretty confident with this)
I did the same test with js SDK @azure/arm-managementgroups
, and got the same result
I suspect the first command is invalid: the scope /providers/Microsoft.Management/managementGroups/providers/Microsoft.Management/managementGroups
in the error is clearly wrong...
What do you get if you run az account management-group show --name non-existing
?
I found the upstream issue which is probably causing this https://github.com/Azure/azure-rest-api-specs/issues/9549
What do you get if you run az account management-group show --name non-existing?
I got the following:
az account management-group show --name non-existing
code: AuthorizationFailed - , The client 'hidden' with object id 'hidden' does not have authorization to perform action 'Microsoft.Management/managementGroups/read' over scope '/providers/Microsoft.Management/managementGroups/non-existing' or the scope is invalid. If access was recently granted, please refresh your credentials.
So, you're right, the issue is caused by the upstream.
I remember that I commented a terraform issue about this: https://github.com/terraform-providers/terraform-provider-azurerm/issues/6091. They patch it on their side.
Same issue with management.v20200501.ManagementGroupSubscription
(but this time, we can not fallback to azure provider because this association doesn't exist)
I share my workaround for the issue with management.v20200501.ManagementGroupSubscription
.
I use Azure SDK and wrap it in a pulumi.dynamic.Resource
.
[Edit]: 🐞 This version does not work - See this version in next comment for a working version
index.ts
:
import { getClientToken } from '@pulumi/azure-nextgen/authorization/latest'
import { ManagementGroupSubscription } from './ManagementGroupSubscription'
async function createManagementGroupSubscription() {
// call to getClientToken should be done outside the pulumi.dynamic.Resource
// see https://github.com/pulumi/pulumi/issues/2580#issuecomment-781559171
const clientToken = await getClientToken()
// usage of management_v20200501.ManagementGroupSubscription from next-gen provider is blocked by https://github.com/pulumi/pulumi-azure-native/issues/553
new ManagementGroupSubscription(
`mg-sub-1`,
{
groupId: <groupId>,
subscriptionId: <subscriptionId>,
},
clientToken.token,
)
- `ManagementGroupSubscription.ts`:
```ts
import * as pulumi from '@pulumi/pulumi'
import { ManagementGroupsAPI } from '@azure/arm-managementgroups'
// NPM alias to be able to instal version 1 in addition to version 2.
// Command: npm install @azure/ms-rest-js1@npm:@azure/ms-rest-js@1
import { TokenCredentials } from '@azure/ms-rest-js1'
// fix for https://github.com/pulumi/pulumi-azure-native/issues/553
function getClient(token: string): ManagementGroupsAPI {
const credentials = new TokenCredentials(token)
return new ManagementGroupsAPI(credentials)
}
/**
* The set of arguments for constructing a ManagementGroupSubscription resource.
*/
export interface ManagementGroupSubscriptionInputs {
/**
* Management Group ID.
*/
groupId: string
/**
* Subscription ID.
*/
subscriptionId: string
}
/**
* The set of arguments for constructing a ManagementGroupSubscription resource.
*/
export interface ManagementGroupSubscriptionArgs {
/**
* Management Group ID.
*/
readonly groupId: pulumi.Input<string>
/**
* Subscription ID.
*/
readonly subscriptionId: pulumi.Input<string>
}
class ManagementGroupSubscriptionProvider implements pulumi.dynamic.ResourceProvider {
private token: string
constructor(token: string) {
this.token = token
}
async check(
olds: ManagementGroupSubscriptionInputs,
news: ManagementGroupSubscriptionInputs,
): Promise<pulumi.dynamic.CheckResult> {
return { inputs: news }
}
async create(inputs: ManagementGroupSubscriptionInputs): Promise<pulumi.dynamic.CreateResult> {
const client = getClient(this.token)
await client.managementGroupSubscriptions.create(inputs.groupId, inputs.subscriptionId)
// do not use the result as it is empty!?!
const id = `/providers/Microsoft.Management/managementGroups/${inputs.groupId}/subscriptions/${inputs.subscriptionId}`
return { id, outs: { groupId: inputs.groupId, subscriptionId: inputs.subscriptionId } }
}
async diff(
id: pulumi.ID,
olds: ManagementGroupSubscriptionInputs,
news: ManagementGroupSubscriptionInputs,
): Promise<pulumi.dynamic.DiffResult> {
if (olds.groupId === news.groupId && olds.subscriptionId === news.subscriptionId) {
return {
changes: false,
stables: ['groupId', 'subscriptionId'],
}
}
throw new Error(
`Error: ManagementGroupSubscription resource does not support changes. Please delete the resource and create a new one`,
)
}
async delete(id: pulumi.ID, props: ManagementGroupSubscriptionInputs): Promise<void> {
const client = getClient(this.token)
await client.managementGroupSubscriptions.deleteMethod(props.groupId, props.subscriptionId)
return
}
}
export class ManagementGroupSubscription extends pulumi.dynamic.Resource {
public readonly groupId!: pulumi.Output<string>
public readonly subscriptionId!: pulumi.Output<number>
constructor(
name: string,
args: ManagementGroupSubscriptionArgs,
token: string,
opts?: pulumi.CustomResourceOptions,
) {
super(new ManagementGroupSubscriptionProvider(token), name, args, opts)
}
}
@mikhailshilkov, another (a bit complex) example of GetClientToken usage // #601
@mikhailshilkov, I just figured out that my previous code doesn't work as the pulumi token is saved in pulumi state. So, when the resource has to be deleted, it uses this token which have expired since. The code is not working!
When I was calling the getClientToken()
from inside the ManagementGroupSubscriptionProvider
, I got the pulumi.errors.RunError: Program run without the Pulumi engine available; re-run using the 'pulumi' CLI
error as mentioned in this comment
Do you have any solution to make it working? I miss something. Thank you
I think I should look at this example: https://github.com/pulumi/examples/blob/master/classic-azure-ts-dynamicresource/cdnCustomDomain.ts
ManagementGroupSubscription
dynamic resourceHere is a fixed version of the ManagementGroupSubscription
dynamic resource, as a workaround of current issue
with management.v20200501.ManagementGroupSubscription
(described above)
Fixes:
@azure/identity
. It does not rely anymore on GetClientToken
function (and does not serialize anymore the pulumi token which was the previous issue)pulumi refresh
(the API used was not exposed in @azure/arm-managementgroups
. Existing resource should be deleted and recreate to add refresh support.index.ts
:
import { ManagementGroupSubscription } from './ManagementGroupSubscription'
// usage of management_v20200501.ManagementGroupSubscription from next-gen provider is blocked by https://github.com/pulumi/pulumi-azure-native/issues/553
new ManagementGroupSubscription(
mg-sub-${subscriptionName}
,
{
groupId:
- `ManagementGroupSubscription.ts`:
```ts
import * as pulumi from '@pulumi/pulumi'
import * as azure from '@pulumi/azure'
import { ManagementGroupsAPI } from '@azure/arm-managementgroups'
import { AzureCliCredential, ClientSecretCredential, TokenCredential } from '@azure/identity'
import { RestError, WebResource } from '@azure/ms-rest-js1'
import { AzureIdentityCredentialAdapter } from './AzureIdentityCredentialAdapter'
// fix for https://github.com/pulumi/pulumi-azure-native/issues/553
function getClient(): ManagementGroupsAPI {
const clientId = process.env['ARM_CLIENT_ID'] || azure.config.clientId
const clientSecret = process.env['ARM_CLIENT_SECRET'] || azure.config.clientSecret
const tenantId = process.env['ARM_TENANT_ID'] || azure.config.tenantId
let tokenCredential: TokenCredential
// If at least one of them is empty, try looking at the env vars.
if (!clientId || !clientSecret || !tenantId) {
pulumi.log.debug('ManagementGroupsAPI: Login with Az CLI.', undefined, undefined, true)
tokenCredential = new AzureCliCredential()
} else {
pulumi.log.debug('ManagementGroupsAPI: Login with clientId and clientSecret.', undefined, undefined, true)
tokenCredential = new ClientSecretCredential(tenantId, clientId, clientSecret)
}
const serviceClientCredentials = new AzureIdentityCredentialAdapter(tokenCredential)
return new ManagementGroupsAPI(serviceClientCredentials)
}
async function getSubscriptionUnderManagementGroupUsingWebResource(
client: ManagementGroupsAPI,
groupId: string,
subscriptionId: string,
): Promise<void> {
const url =
'https://management.azure.com' +
'/providers/Microsoft.Management/managementGroups/' +
groupId +
'/subscriptions/' +
subscriptionId +
'?api-version=2020-05-01'
const webResource = new WebResource(
url, // url
'GET', // method
undefined, // body
undefined, // query
undefined, // headers
undefined, // streamResponseBody
true, // withCredentials
undefined, // abortSignal
30000, // timeout in ms
)
const response = await client.sendRequest(webResource)
if (response.status !== 200) {
throw new RestError(
`subscriptions '${subscriptionId}' not found under management group '${groupId}. Cause: ${response.bodyAsText}'`,
undefined,
response.status,
webResource,
response,
)
}
return
}
/**
* The set of arguments for constructing a ManagementGroupSubscription resource.
*/
export interface ManagementGroupSubscriptionInputs {
/**
* Management Group ID.
*/
groupId: string
/**
* Subscription ID.
*/
subscriptionId: string
}
/**
* The set of arguments for constructing a ManagementGroupSubscription resource.
*/
export interface ManagementGroupSubscriptionArgs {
/**
* Management Group ID.
*/
readonly groupId: pulumi.Input<string>
/**
* Subscription ID.
*/
readonly subscriptionId: pulumi.Input<string>
}
class ManagementGroupSubscriptionProvider implements pulumi.dynamic.ResourceProvider {
/**
* Creates an ID that does not change when management group changes (uses only subscriptionId)
*/
buildId(_groupId: string, subscriptionId: string): string {
return `/providers/Microsoft.Management/managementGroups/subscriptions/${subscriptionId}`
}
async check(
_olds: ManagementGroupSubscriptionInputs,
news: ManagementGroupSubscriptionInputs,
): Promise<pulumi.dynamic.CheckResult> {
return { inputs: news }
}
async read(_id: pulumi.ID, props: ManagementGroupSubscriptionInputs): Promise<pulumi.dynamic.ReadResult> {
const client = getClient()
await getSubscriptionUnderManagementGroupUsingWebResource(client, props.groupId, props.subscriptionId)
return {
id: this.buildId(props.groupId, props.subscriptionId),
props,
}
}
async create(inputs: ManagementGroupSubscriptionInputs): Promise<pulumi.dynamic.CreateResult> {
const client = getClient()
await client.managementGroupSubscriptions.create(inputs.groupId, inputs.subscriptionId)
const id = this.buildId(inputs.groupId, inputs.subscriptionId)
return { id, outs: { groupId: inputs.groupId, subscriptionId: inputs.subscriptionId } }
}
async diff(
_id: pulumi.ID,
olds: ManagementGroupSubscriptionInputs,
news: ManagementGroupSubscriptionInputs,
): Promise<pulumi.dynamic.DiffResult> {
if (olds.subscriptionId === news.subscriptionId) {
return {
changes: olds.groupId !== news.groupId,
stables: ['subscriptionId'],
}
}
throw new Error(
`Error: ManagementGroupSubscription resource does not support change of 'subscriptionId' input. Please delete the resource and create a new one`,
)
}
async update(
_id: pulumi.ID,
_olds: ManagementGroupSubscriptionInputs,
news: ManagementGroupSubscriptionInputs,
): Promise<pulumi.dynamic.UpdateResult> {
const client = getClient()
await client.managementGroupSubscriptions.create(news.groupId, news.subscriptionId)
return { outs: { groupId: news.groupId, subscriptionId: news.subscriptionId } }
}
async delete(_id: pulumi.ID, props: ManagementGroupSubscriptionInputs): Promise<void> {
const client = getClient()
await client.managementGroupSubscriptions.deleteMethod(props.groupId, props.subscriptionId)
return
}
}
export class ManagementGroupSubscription extends pulumi.dynamic.Resource {
public readonly groupId!: pulumi.Output<string>
public readonly subscriptionId!: pulumi.Output<number>
constructor(name: string, args: ManagementGroupSubscriptionArgs, opts?: pulumi.CustomResourceOptions) {
super(new ManagementGroupSubscriptionProvider(), name, args, opts)
}
}
AzureIdentityCredentialAdapter.ts
:
⚠️ The AzureIdentityCredentialAdapter
file should be duplicated from '@azure/arm-managementgroups/node_modules/@azure/ms-rest-js/lib/credentials/azureIdentityTokenCredentialAdapter', because the class is not exported at module level. And importing this file directly is not supported by pulumi provider serialization.
// File duplicated from '@azure/arm-managementgroups/node_modules/@azure/ms-rest-js/es/lib/credentials/azureIdentityTokenCredentialAdapter'
// Because class is not exported at module level
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { TokenCredential } from '@azure/identity'
// NPM alias to be able to instal version 1 in addition to version 2.
// Command: npm install @azure/ms-rest-js1@npm:@azure/ms-rest-js@1
import { ServiceClientCredentials, WebResource, Constants } from '@azure/ms-rest-js1'
// import { ServiceClientCredentials } from "./serviceClientCredentials";
// import { Constants as MSRestConstants } from "../util/constants";
// import { WebResource } from "../webResource";
// import { TokenCredential } from "@azure/core-auth";
// import { TokenResponse } from "./tokenResponse";
const DEFAULT_AUTHORIZATION_SCHEME = 'Bearer'
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
/**
* TokenResponse is defined in `@azure/ms-rest-nodeauth` and is copied here to not
* add an unnecessary dependency.
*/
interface TokenResponse {
readonly tokenType: string
readonly accessToken: string
readonly [x: string]: unknown
}
/**
* This class provides a simple extension to use {@link TokenCredential} from `@azure/identity` library to
* use with legacy Azure SDKs that accept {@link ServiceClientCredentials} family of credentials for authentication.
*/
export class AzureIdentityCredentialAdapter implements ServiceClientCredentials {
private azureTokenCredential: TokenCredential
private scopes: string | string[]
constructor(
azureTokenCredential: TokenCredential,
scopes: string | string[] = 'https://management.azure.com/.default',
) {
this.azureTokenCredential = azureTokenCredential
this.scopes = scopes
}
public async getToken(): Promise<TokenResponse> {
const accessToken = await this.azureTokenCredential.getToken(this.scopes)
if (accessToken !== null) {
const result: TokenResponse = {
accessToken: accessToken.token,
tokenType: DEFAULT_AUTHORIZATION_SCHEME,
expiresOn: accessToken.expiresOnTimestamp,
}
return result
} else {
throw new Error('Could find token for scope')
}
}
public async signRequest(webResource: WebResource): Promise<WebResource> {
const tokenResponse = await this.getToken()
webResource.headers.set(
Constants.HeaderConstants.AUTHORIZATION,
`${tokenResponse.tokenType} ${tokenResponse.accessToken}`,
)
return webResource
}
}
Any traction on this? Apparently still a bug in 2022. :/
Apparently, TF provider accepted a bug for this: https://github.com/hashicorp/terraform-provider-azurerm/pull/6668/files
The issue still occurs. Is there any chance to progress on this one?
With ARM, management group can be created at
tenant
level or atmanagementGroup
level.It seems that pulumi next gen
management.v20200501.ManagementGroup
only supportstenant
level. Andtenant
level requires very high permission (actuallyManagement Group Contributor
at the root scope/
).Indeed, when I tried to create a "mg-test" Management Group, child of the "Tenant root group", I got the following error:
You can notice that error did not report correctly the scope, because the scope should be
/
in this case.Pulumi code :
I notice you create a new resource PolicyDefinitionAtManagementGroup (for similar issue?)