How to use Pulumi Output<string> as a string in .NET - c#

I have a basic Pulumi build for keycloak where I set up a realm, create a scope, create a client, and update teh scopes for my client.
class RealmBuild : Stack
{
public RealmBuild()
{
var realm = new Realm("ExampleRealm-realm", new RealmArgs
{
RealmName = "ExampleRealm"
});
var recipemanagementScope = ScopeFactory.CreateScope(realm.Id, "recipe_management");
var recipeManagementPostmanMachineClient = ClientFactory.CreateClientCredentialsFlowClient(realm.Id,
"recipe_management.postman.machine",
"974d6f71-d41b-4601-9a7a-a33084484682",
"RecipeManagement Postman Machine",
"https://oauth.pstmn.io");
recipeManagementPostmanMachineClient.ExtendDefaultScopes(recipemanagementScope.Name);
}
}
public static class ClientExtensions
{
public static void ExtendDefaultScopes(this Client client, params Output<string>[] scopeNames)
{
var defaultScopeName = $"default-scopes-for-{client.Name.Apply(x => x)}";
var defaultScopes = new ClientDefaultScopes(defaultScopeName, new ClientDefaultScopesArgs()
{
RealmId = client.RealmId,
ClientId = client.Id,
DefaultScopes =
{
"openid",
"profile",
"email",
"roles",
"web-origins",
scopeNames,
},
});
}
}
public class ClientFactory
{
public static Client CreateClientCredentialsFlowClient(Output<string> realmId,
string clientId,
string clientSecret,
string clientName,
string baseUrl)
{
return new Client($"{clientName.ToLower()}-client", new ClientArgs()
{
RealmId = realmId,
ClientId = clientId,
Name = clientName,
StandardFlowEnabled = false,
Enabled = true,
ServiceAccountsEnabled = true,
AccessType = "CONFIDENTIAL",
BaseUrl = baseUrl,
AdminUrl = baseUrl,
ClientSecret = clientSecret,
BackchannelLogoutSessionRequired = true,
BackchannelLogoutUrl = baseUrl
});
}
}
The problem is, I am getting this error around my scopes:
Diagnostics:
keycloak:openid:ClientDefaultScopes (default-scopes-for-Calling [ToString] on an [Output<T>] is not supported.
To get the value of an Output<T> as an Output<string> consider:
1. o.Apply(v => $"prefix{v}suffix")
2. Output.Format($"prefix{hostname}suffix");
See https://pulumi.io/help/outputs for more details.
This function may throw in a future version of Pulumi.):
error: Duplicate resource URN 'urn:pulumi:dev::KeycloakPulumiStack::keycloak:openid/clientDefaultScopes:ClientDefaultScopes::default-scopes-for-Calling [ToString] on an [Output<T>] is not supported.
To get the value of an Output<T> as an Output<string> consider:
1. o.Apply(v => $"prefix{v}suffix")
2. Output.Format($"prefix{hostname}suffix");
See https://pulumi.io/help/outputs for more details.
This function may throw in a future version of Pulumi.'; try giving it a unique name
I tried something like this as well var defaultScopeName = Output.Format($"default-scopes-for-{client.Name}");, but I can't pass that into the name for ClientDefaultScopes
I did look at the docs to see if anything stuck out as an issue, but I'm clearly missing something.

Rule number 1 with Pulumi outputs: Anything you return from an apply() will still be an Output, even if it looks like it should be a string.
In other words, on this line of code:
var defaultScopeName = $"default-scopes-for-{client.Name.Apply(x => x)}";
defaultScopeName is Output<string>.
However, the x variable in the lambda is in fact a string rather than an output.
The other item to note is that the name of a resource (so the first argument) cannot be an Output. So in your code:
var defaultScopeName = $"default-scopes-for-{client.Name.Apply(x => x)}";
var defaultScopes = new ClientDefaultScopes(defaultScopeName, new ClientDefaultScopesArgs()
{
RealmId = client.RealmId,
ClientId = client.Id,
DefaultScopes =
{
"openid",
"profile",
"email",
"roles",
"web-origins",
scopeNames,
},
});
because defaultScopeName is an Output, this won't work.
You could create the resource inside of the apply():
var defaultScopea = $"default-scopes-for-{client.Name.Apply(x =>
return new ClientDefaultScopes(x, new ClientDefaultScopesArgs()
{
RealmId = client.RealmId,
ClientId = client.Id,
DefaultScopes =
{
"openid",
"profile",
"email",
"roles",
"web-origins",
scopeNames,
},
});
)}";
however, this may mean that the resource won't appear in any previews (see the note in the Apply section of the Inputs and Outputs page in the Pulumi docs).
So what's the answer here? it looks like you're setting the ClientName to be a string value earlier in the code, so I'd use the same variable that you're setting there.

You can't mix and match string and Output<string> values. Instead, you need to transform any output and append your static list to the list of resolved values:
var defaultScopeName = Output.Format($"default-scopes-for-{client.Name}");
var defaultScopes = new ClientDefaultScopes("some-scope-name", new ClientDefaultScopesArgs()
{
RealmId = client.RealmId,
ClientId = client.Id,
DefaultScopes = Output.All(scopeNames).Apply(names =>
new[] { "openid", "profile", "email", "roles", "web-origins", }
.Concat(names)),
});
Note that Output.Format is used for string formatting, Output.All is used to convert to Output<string[]> and .Apply is used to transform the array. You can learn more in Inputs and Outputs.

Currently, Pulumi only supports string types for the name of a resource.
Since
var defaultScopeName = $"default-scopes-for-{client.Name.Apply(x => x)}";
is using an output of a resource, defaultScopeName is type Output<string> and can't be used for the resource name in the line,
var defaultScopes = new ClientDefaultScopes(defaultScopeName, new ClientDefaultScopesArgs()
If I'm reading the code correctly, you specify clientName and use it to set client.Name. So, I would just pass in clientName and use that instead of client.Name. And, that should work since it's a basic type all the way through.

Related

Create Lambda proxy integration inside a non-root resource in API Gateway using AWS SDK with C#

The bounty expires in 7 days. Answers to this question are eligible for a +100 reputation bounty.
Harun Ćerim is looking for an answer from a reputable source:
A detailed explanation of how Lambda integration can be done on a non-root resource inside an API Gateway where routes are currently mapped to an underlying Lambda function and its underlying routes
I am trying to create a proxy integration for Lambda invocation from API Gateway for a specific route (example.com/auth/{proxy+}, example.com/user/{proxy+}, etc..).
I am creating a script for automated infrastructure initialization and deployment (without using CloudFormation, Terraform, etc.) directly from .NET script that is planned to be available as an API.
Predefined infrastructure contains: Route53 config, created API Gateway with custom domain.
Dynamic infrastructure contains: S3 and Lambda together with API Gateway modifications and deployment.
Once the bucket for a new service is created and the built app is pushed to the bucket, new Lambda function is created, configured and published. The last thing that is left is to create a new resource (route) that will invoke underlying Lambda function and its underlying routes (e.g. example.com/auth/register).
The issue is that when I create an integration on a non-root resource, Lambda function cannot be found or the Uri is not specified as it should be (this is something I am trying to figure out).
Here is the simplified code that I wrote to accomplish this (I will exclude Lambda function and S3 bucket created and show only API gateway and Lambda resource policy updates as they are relevant here). Important to note is that this code produces the same results as if it would be done via AWS console. Also, this code produces a working solution if the route is not specified (e.g. example.com/register)
var functionArn = await Lambda.GetFunctionArn(accessKey, secretKey, region, lambdaFunction);
var pathResponse = await c.CreateResourceAsync(new CreateResourceRequest
{
ParentId = rootId,
PathPart = path,
RestApiId = apiId
});
await c.PutMethodAsync(new PutMethodRequest
{
AuthorizationType = "NONE",
HttpMethod = "ANY",
ResourceId = pathResponse.Id,
RestApiId = apiId
});
var proxyResponse = await c.CreateResourceAsync(new CreateResourceRequest
{
ParentId = pathResponse.Id,
PathPart = "{proxy+}",
RestApiId = apiId
});
await c.PutMethodAsync(new PutMethodRequest
{
AuthorizationType = "NONE",
HttpMethod = "ANY",
ResourceId = proxyResponse.Id,
RestApiId = apiId
});
await Lambda.AddPermissions(account, accessKey, secretKey, region, lambdaFunction, apiId, path);
await c.PutIntegrationAsync(new PutIntegrationRequest
{
HttpMethod = "ANY",
IntegrationHttpMethod = "POST",
ResourceId = pathResponse.Id,
RestApiId = apiId,
PassthroughBehavior = "WHEN_NO_MATCH",
Type = IntegrationType.AWS_PROXY,
Uri = $"arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{functionArn}/invocations"
});
await c.PutIntegrationAsync(new PutIntegrationRequest
{
HttpMethod = "ANY",
IntegrationHttpMethod = "POST",
ResourceId = proxyResponse.Id,
RestApiId = apiId,
PassthroughBehavior = "WHEN_NO_MATCH",
Type = IntegrationType.AWS_PROXY,
Uri = $"arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{functionArn}/invocations"
});
var deployment = await c.CreateDeploymentAsync(new CreateDeploymentRequest
{
Description = $"API deployment to {environment}",
RestApiId = apiId,
StageName = environment
});
return deployment.Id;
where Lambda.AddPermissions is as follows:
var basePermission = await c.AddPermissionAsync(new AddPermissionRequest
{
Action = "lambda:InvokeFunction",
FunctionName = name,
Principal = "apigateway.amazonaws.com",
SourceArn = $"arn:aws:execute-api:{region}:{account}:{apiId}/*/*/{path}/*",
StatementId = Guid.NewGuid().ToString()
});
var proxyPermission = await c.AddPermissionAsync(new AddPermissionRequest
{
Action = "lambda:InvokeFunction",
FunctionName = name,
Principal = "apigateway.amazonaws.com",
SourceArn = $"arn:aws:execute-api:{region}:{account}:{apiId}/*/*/{path}",
StatementId = Guid.NewGuid().ToString()
});
return new List<string>
{
basePermission.Statement,
proxyPermission.Statement
};
Is there an issue with SourceArn specifications? I first created them through the AWS console (they are automatically created when the integration is created for Lambda) and they are the same.
Again, this all works when there is no path (non-root resource).

Unit Test in Azure Function C#

I want to unit test my azure function API by sending mock request and response data. But my test is getting failed even if i pass same Json data on both request and response.
TestCode
[TestMethod]
public async Task ClinicReadTestMethod()
{
//Arrange
//var clinicRequest = new
//{
// Id = "1",
// OpenIdProvider = "Google",
// Subject = "Test",
// Name = "Test",
// Address = "Test",
// Email = "Test",
// Phone = "Test",
// Notes = "Test"
//};
var query = new Dictionary<string, StringValues>();
query.Add("openIdProvider", "Google");
query.Add("subject", "Test");
//var body = JsonSerializer.Serialize(clinicRequest);
var logger = Mock.Of<ILogger>();
var client = Mock.Of<CosmosClient>();
ContentResultFactory contentResultFactory = new ContentResultFactory();
//Act
var testFunction = new ClinicReadFunction(contentResultFactory);
var result = await testFunction.Run(TestFactory.HttpRequestSetup(query), client, logger); //fixme
var resultObject = JsonSerializer.Serialize(result as ContentResult);
//Assert
var clinicResponse = new
{
Id = "1",
openIdProvider = "Google",
subject = "Test",
Name = "Test",
Address = "Test",
Email = "Test",
Phone = "Test",
Notes = "Test"
};
var resultBody = JsonSerializer.Serialize(clinicResponse);
//var res = contentResultFactory.CreateContentResult(HttpStatusCode.OK);
Assert.AreEqual(resultBody, resultObject);
}
}
This is how my azure function looks like. It is taking two parameters and returning the response. I have tried to mock the data for unit test still no success. If anyone have idea how to unit test this azure function please let me know.
//AzureFunction
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "")] HttpRequest req,
[CosmosDB(
databaseName: "",
containerName: "",
Connection = ""
)] CosmosClient client,
ILogger log)
{
string subject = req.Query["sub"];
if (!Enum.TryParse(req.Query["idp"], out OpenIdProvider openIdProvider) || string.IsNullOrEmpty(subject))
{
var message = "";
log.LogWarning();
return _contentResultFactory.CreateContentResult(message, HttpStatusCode.BadRequest);
}
var query = client.GetContainer("", "").GetItemLinqQueryable<Clinic>()
.Where(x => x.OpenIdProvider == openIdProvider && x.Subject == subject);
Clinic clinic;
using (var iterator = query.ToFeedIterator())
clinic = (await iterator.ReadNextAsync()).FirstOrDefault();
if (clinic == null)
{
log.LogWarning();
return _contentResultFactory.CreateContentResult();
}
var response = new ClinicReadResponse(clinic);
return _contentResultFactory.CreateContentResult(response, HttpStatusCode.OK);
}
//TestFactory
public static HttpRequest HttpRequestSetup(Dictionary<string, StringValues> query)
{
var context = new DefaultHttpContext();
var request = context.Request;
request.Query = new QueryCollection(query);
request.Method = "GET";
return request;
}
In both your Clinic objects, your are generating a new GUID for the ID by calling System.Guid.NewGuid. Assuming the JSON generated from each object is the same shape (they will need to be if you want them to match), the values of each ID property will be different. Since the IDs are different, your JSON strings are not equal, therefore causing the failure.
Here is a post that will show you how to manually create a Guid. You can use this to ensure your IDs are of the same value when testing.
Assigning a GUID in C#
I don't know what your Azure Function code looks like, but your test's setup to make an HTTP request tells me you're calling the method tied to the Http Trigger. Consider the scope of what your method is doing; if it is large (or is calling other methods), this will increase the chances of your test breaking as you change the Azure Function over time. To help future-proof your test make sure the method it's calling has a single responsibility. This will make debugging your code easier to do if a change does make your test fail, and will lessen the likelihood of needing to edit your test to accommodate for code changes.

How do you set userExtension/optional claims using the graph api?

I am writing code to set optional claims. I was successful in setting the claim using PowerShell Set-AzureADUserExtension and validate it with Get-AzureADUserExtension. But we need to set the value using the graphApi.
The graphApi call that I tried with some variations are:
var application = (await graphClient.Applications.Request().Filter($"appId eq '{appClientId}'").GetAsync()).SingleOrDefault();
var schemas = await graphClient.Applications[application.Id].ExtensionProperties.Request().GetAsync();
var schema = schemas.CurrentPage.Single(x => x.Name == $"extension_{appClientId.Replace("-", string.Empty)}_clShops");
await graphClient.Users[userId].Extensions.Request().AddAsync (new OpenTypeExtension
{
ODataType = "Microsoft.DirectoryServices.User",
AdditionalData = new Dictionary<string, object> { { schema.Name, "1,2,3" } },
ExtensionName = schema.Id
});
I tried changing the values in OpenTypeExtension but I have not been able to alter the value for that user (checking with the Get-AzureADUserExtension powershell command).
My question is what graphApi function do i need to call so set the optional claim to get the same result as calling Set-AzureADUserExtension?
c# graphApi sdk version is Microsoft.Graph Version=3.35.0
figured it out after some more tests:
optionly you can get the name using:
var application = (await graphClient.Applications.Request().Filter($"appId eq '{appClientId}'").GetAsync()).SingleOrDefault();
var schemas = await graphClient.Applications[application.Id].ExtensionProperties.Request().GetAsync();
var schema = schemas.CurrentPage.Single(x => x.Name.EndsWith("name of claim"));
or by convention (replace schema.Name with): $"extension_{appId}_{'name of claim'}"
await graphClient.Users[userId].Request().UpdateAsync(new Microsoft.Graph.User
{
AdditionalData = new Dictionary<string, object>
{
{ schema.Name,value}
}
});

C# POST Request with arrays being sent

I want to send a POST request in c# and i need the following sent through
"jsonrpc": "2.0",
"id": "12345",
"method": "my method",
"params": {
"api_key": "my api key",
"preset_id": "my preset id"
}
I tried using
using (WebClient client = new WebClient ())
{
byte [] response =
client.UploadValues ("my url", new NameValueCollection ()
{
{ "jsonrpc", "2.0" },
{ "id", "12345"},
{ "method", "my method"},
{ "params", ""}
});
string result = System.Text.Encoding.UTF8.GetString (response);
}
But i couldnt make the params an array, Please help, Thank you
It appears that you are asking for the parameters to be in an array, but they are actually shown as a "subclass". If the values were in an array, they should have square brackets around them.
However, both results are easy to achieve using anonymous (or real) classes (which I much prefer over embedding the property names in quoted text (makes future modifications much easier to implement).
var parameters = new
{
api_key = "my api key",
preset_id = "my preset id"
};
var json = new
{
jsonrpc = "2.0",
id = "12345",
method = "my method",
#params = parameters
};
string sResult = (new System.Web.Script.Serialization.JavaScriptSerializer()).Serialize(json);
The above code will result in the same output that you have shown. If you want an actual array instead, you can change the parameters definition to:
var parameters = new NameValueCollection();
parameters.Add("api_key", "my api key");
parameters.Add("preset_id", "my preset id");
Note that I used the .Net framework json serializer (from System.Web.Extensions), but you can use the serializer of your choice (we generally use NewtonSoft's JsonConvert).

IdentityServer "invalid_client" error always returned

I'm trying to use IdentityServer3, but don't know why I'm getting "invalid_client" error always, always no matter what I do.
This is the code I'm using:
//Startup.cs (Auth c# project)
public void Configuration(IAppBuilder app) {
var inMemoryManager = new InMemoryManager();
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(inMemoryManager.GetClients())
.UseInMemoryScopes(inMemoryManager.GetScopes())
.UseInMemoryUsers(inMemoryManager.GetUsers());
var options = new IdentityServerOptions {
Factory = factory,
RequireSsl = false
};
app.UseIdentityServer(options);
}
InMemoryManager helper.
//InMemoryManager.cs
public class InMemoryManager {
public List<InMemoryUser> GetUsers() {
return new List<InMemoryUser> {
new InMemoryUser {
Username = "alice",
Password = "password",
Subject = "2",
Claims = new [] {
new Claim("User name", "Alice")
}
}
};
}
public IEnumerable<Scope> GetScopes() {
return new[] {
new Scope {
Name = "api1",
DisplayName = "API 1"
}
};
}
public IEnumerable<Client> GetClients() {
return new[] {
new Client {
ClientName = "Silicon on behalf of Carbon Client",
ClientId = "carbon",
Enabled = true,
//AccessTokenType = AccessTokenType.Reference,
Flow = Flows.ResourceOwner,
ClientSecrets = new List<Secret> {
new Secret("secret".Sha256())
},
AllowedScopes = new List<string> {
"api1"
}
}
};
}
}
This is the result I always get.
I'm using postman to try the Auth Server, but I always get that error. I've read another solutions but none seeme to works, I don't know what else to try.
Cheers.
Just add the client_secret: secret in your Body. It will work!
Late answer, but for me this happened following the IdentityServer 4 tutorial when trying to log in with a username and password. I used the code from the first tutorial (using client credentials), and modified the client to use passwords. Afterwards, I kept getting this error.
To fix it, in the IdentityServer project, config.cs, in the GetClients method, set AllowedGrantTypes to GrantTypes.ResourceOwnerPassword, and change ClientId from client to ro.client (or whatever the client name is that you use in the Client project's program.cs).
Your request shoud be as follows:
Authorisation header with clientId/clientSecret. carbon/secret in Your case.
In Body. username/password shoud be alice/password in Your case. If Your don't need to refresh tokens, You might exclude offline_access scope from request.

Categories

Resources