I'm using Apama v10.3.1. I'm using the built-in Apama container of a Cumulocity installation, that is all I'm uploading is a monitor, not an entire Apama project. In my Apama monitor I'm executing an HTTP GET request against the Cumulocity REST API to obtain additional parameters I need for my monitor processing.
My problem is that when executing the HTTP request I need to provide a user and password, otherwise I get a 401 error. Since I do not want to hard code the user and password in my monitor, is there any way to use the credentials the built-in Apama container uses for communicating with Cumulocity? Since Apama communicates with Cumulocity under the hood for exchanging events, measurements and alike, I would assume that there are credentials available somewhere. Is that the case and if so, how can I tell my Apama monitor to use these credentials?
Here is an extract of the code:
monitor SampleAlarmRules {
action onload() {
monitor.subscribe(Alarm.CHANNEL);
monitor.subscribe(FindManagedObjectResponse.CHANNEL);
on all Alarm(type = "ax_UnavailabilityAlarm") as a {
onAlarm(a.source);
}
}
action onAlarm(string source) {
integer reqId := integer.getUnique();
send FindManagedObject(reqId, source, new dictionary<string,string>) to FindManagedObject.CHANNEL;
on FindManagedObjectResponse(reqId = reqId) as resp
and not FindManagedObjectResponseAck(reqId) {
ManagedObject dev := resp.managedObject;
dictionary<string, string> httpConfig := {
HttpTransport.CONFIG_AUTH_TYPE:"HTTP_BASIC"
//HttpTransport.CONFIG_USERNAME:"someUser",
//HttpTransport.CONFIG_PASSWORD:"somePassword"
};
HttpTransport httpTransport := HttpTransport.getOrCreateWithConfigurations("someBaseUrl", 80, httpConfig);
Request request := httpTransport.createGETRequest("/inventory/managedObjects/5706999?withParents=true");
request.execute(handleResponse);
}
}
action handleResponse(Response response) {
JSONPlugin json := new JSONPlugin;
if response.isSuccess(){
switch (response.payload.data as payload) {
case string: {
}
default: {
}
}
}
else {
print "###Request failed. Response status is: " + response.statusCode.toString() + " | " + response.statusMessage;
}
}
}
With this configuration (user and password commented) I get the following print statement:
Request failed. Response status is: 401 | Unauthorized
When enabling the user and password, the request executes successfully. However, I do not want to hard code the user and password in here.
Also, is there a way to get the current tenant from an environment variable or anything like that so that I don't have to hard code the base URL?
Thanks
Mathias
Yes, it is possible to do that because Cumulocity passes these as environment variables to all microservice including the Apama microservice.
You should be able to use the com.apama.correlator.Component event to access the environment variables. Use
Component.getInfo("envp") to get the dictionary of environment properties and then lookup the interested variables. You can see the list of environment variables at http://www.cumulocity.com/guides/reference/microservice-runtime/#environment-variables
So for your use case something like following will work:
using com.apama.correlator.Component;
...
monitor test {
action onload() {
dictionary<string,string> envp := Component.getInfo("envp");
dictionary<string, string> httpConfig := {
HttpTransport.CONFIG_AUTH_TYPE:"HTTP_BASIC",
HttpTransport.CONFIG_USERNAME:envp["C8Y_USER"],
HttpTransport.CONFIG_PASSWORD:envp["C8Y_PASSWORD"]
};
HttpTransport httpTransport := HttpTransport.getOrCreateWithConfigurations("someBaseUrl", 80, httpConfig);
Request request := httpTransport.createGETRequest("/inventory/managedObjects/5706999?withParents=true");
request.execute(handleResponse);
}
}
Similarly, you can access the tenant name using environment variable C8Y_TENANT.
Please note that these environment variables are available only in the cloud. If you want to do the same or test the code locally when using it with Cumulocity transport added by yourself, without changing the code, you can manually define the same environment variables in the run configuration of the Designer so that they are available there as well.
Related
I'm making a multi tenant app using mongo db and would like to know what the proper procedure between switching between databases is. I know I can get a new reference to a database using the db() command:
const client = await MongoClient.connect(url);
client.mainDb = client.db('main');
app.set('mongoClient', client);
On bootup I get and store a reference to my main for all my global app data. Then each request also passes in a tenant id. I'm using Feathersjs which provides me with a hook for every request before and after.
In my before hook, I get a reference to the clients data and store it to be used during that singular request:
app.hooks({
before: {
all: [(context) => {
// Run before all API requests
const tenant = context.params?.query?.$tenant;
const tenantDbName = ... // some logic to query the tenant db name
const client = context.app.get('mongoClient');
context.params.tenantDb = client.db(tenantDbName);
}]
}
}
After the request, I'm unclear on if I should do anything to cleanup the connection. Do I just let the garbage collector clean it up since its request that was made which has ended? Or is there a function in Mongo to clean it up?
app.hooks({
after: {
all: [(context) => {
// Cleanup DB or reset connection?
context.params.tenantDb = null;
}]
}
}
I just need to ensure that the next request doesn't use a previous requests database as this could serve them other users data.
Trying out GetStream for Swift, I am not able to add an activity.
class MyActivity : Activity {}
...
let client = Client(apiKey: <MyApiKey>, appId: <MyApiKey>, token: <Token>)
let ericFeed = client.flatFeed(feedSlug: "user", userId: "eric")
let activity = MyActivity(actor: "eric", verb: "waves", object: "picture:10", foreignId: "picture:10")
ericFeed.add(activity) { result in
print("!result!")
print(result)
}
The token is generated server-side, is in form eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZXJpYyJ9.AAAAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAA-AAAAAAAA and:
client.currentUserId returns eric (So, the token is correct?)
The Callback of ericFeed.add(activity) is not called
On my app's dashboard log, I see the attempt of adding the activity as failed, with error 403
I tried different ids (with different tokens), both actor: "eric" and actor: "user:eric". What could have gone wrong?
The code to generate the token (php server) is:
$userId = "eric";
$client = new GetStream\Stream\Client(<MyApiKey>, <MyAppSecret>);
$userToken = $client->createUserSessionToken($userId);
And I receive to logs on my dashboard:
There is a couple things that needs to keep in mind.
First of all, probably your client was deallocated when the request was ended and that's why the callback wasn't called, but logs could show you that the request was done. I suggest you to use a shared Client instance and it would be easy to use. To setup a shared Client you need to write this:
Client.config = .init(apiKey: "<MyApiKey>", appId: "<MyApiKey>", token: "<Token>")
More about the Client setup in wiki page.
The second important thing, that you have to create/update your Stream user. From the server side you are getting your Token with the Stream userId and can request the Stream user. The easiest way is to call Client.shared.create(user:) where user would be created/updated. So, it's still a part of the Stream Client setup:
Client.shared.create(user: GetStream.User(id: Client.shared.currentUserId!)) { result in
// Update the client with the current user.
Client.shared.currentUser = try? result.get()
// Do all your requests from here. Reload feeds and etc.
}
More info in docs.
I suggest you to create feeds with only feedSlug parameter and the Stream userId would be taken from the Token. But it would be Optional, because the currentUserId is optional. For example:
let ericFeed = Client.shared.flatFeed(feedSlug: "user")
ericFeed?.add(activity)
And for your activities, Stream clients should always use the current Stream user as an actor. So, we need to update the definition of your MyActivity.
Finally, here is your code that should works:
// Define your activity.
class MyActivity: EnrichedActivity<GetStream.User, String, DefaultReaction> {
// ...
}
// Setup Stream Client.
Client.config = .init(apiKey: <MyApiKey>, appId: <MyApiKey>, token: <Token>)
// Setup the current user.
Client.shared.getCurrentUser {
let ericFeed = Client.shared.flatFeed(feedSlug: "user")
let activity = MyActivity(actor: Client.shared.currentUser!, verb: "waves", object: "picture:10", foreignId: "picture:10")
ericFeed?.add(activity) { result in
print("!result!")
print(result)
}
}
I am using IdentityServer4 with two external Idp's, one with WSFederation (ADFS) and one with SAML.
For the SAML implementation I use the commercial product ComponentSpace SAML 2 for ASP.Net Core. I use the middleware-based config.
Logging it with both Idp's works perfectly, but now I have the situation where, depending on the client, I need to pass extra parameters to the SAML AuthnRequest. I know how to pass this extra parameter in the request (I can use the OnAuthnRequestCreated from the middleware), but what I don't know is how to test at that point from where the request is coming, i.e. from which client.
I have control of the client so I could also pass extra acr_values (which I think can be used to pass custom data), but again I don't know how to get them in the OnAuthnRequestCreated event as shown in the code below.
Any help would be much appreciated.
services.AddSaml(Configuration.GetSection("SAML"));
services.AddAuthentication()
.AddWsFederation("adfs", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
//...rest of config (SSO is working)
})
.AddSaml("saml", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
//...rest of config (SSO is working)
options.OnAuthnRequestCreated = request =>
{
//Here I would need to know from which client the request is coming (either by client name or url or acr_values or whatever)
//to be able to perform conditional logic. I've checked on the request object itself but the info is not in there
return request;
};
});
The request parameter is the SAML AuthnRequest object. It doesn't include client information etc.
Instead of the OnAuthnRequestCreated event, in your Startup class you can add some middleware as shown below. You can call GetRequiredService to access any additional interfaces (eg IHttpContextAccessor) you need to retrieve the client information.
app.Use((context, next) =>
{
var samlServiceProvider =
context.RequestServices.GetRequiredService<ISamlServiceProvider>();
samlServiceProvider.OnAuthnRequestCreated += authnRequest =>
{
// Update authn request as required.
return authnRequest;
};
return next();
});
Thanks ComponentSpace for the reply. I didn't get it to work directly with your solution by using app.Use((context, next)) => ... but your comment on GetRequiredService pointed me into the direction to find the solution like below. Basically I'm getting the IHttpContextAccessor which I can then use to parse the query string. I then get the ReturnUrl from this query string and use the IIdentityServerInteractionService to get the AuthorizationContext object, which contains what I need to build my custom logic.
So thanks again for pointing me into the right direction.
//build and intermediate service provider so we can get already configured services further down this method
var sp = services.BuildServiceProvider();
services.AddAuthentication()
.AddSaml("SamlIdp", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.OnAuthnRequestCreated = request =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var queryStringValues = HttpUtility.ParseQueryString(httpContextAccessor.HttpContext.Request.QueryString.Value);
var interactionService = sp.GetService<IIdentityServerInteractionService>();
var authContext = interactionService.GetAuthorizationContextAsync(queryStringValues["ReturnUrl"]).Result;
//authContext now contains client info and other useful stuff to help build further logic to customize the request
return request;
};
});
When I try to call this https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-metadata api in microsoft flow I always get this error with 400 bad request.
I edited my authorization header regarding this answer https://stackoverflow.com/a/22029178/10389562 but couldn't get what am I doing wrong.
Method: GET
Uri: https://myaccount.blob.core.windows.net/containername/blobname?comp=metadata
Headers :
{
"Authorization": "SharedKey storageaccountname: primary key in the storage
account properties",
"x-ms-date": "Thu, 21 Sep 2018 23:45:00 GMT",
"x-ms-version": "2018-03-28"
}
After call this API I got this output
<?xml version="1.0" encoding="utf-8"?><Error>
<Code>InvalidAuthenticationInfo</Code><Message>Authentication information is
not given in the correct format. Check the value of Authorization header.
RequestId:f3b3051b-601e-00a4-4b3c-51c58d000000
Time:2018-09-20T23:46:40.6659210Z</Message></Error>
Thanks for any help
Update
In Microsoft Flow, calling Rest Api against Azure Storage seems not a valid way. Authorization needs x-ms-* headers sent by the flow(like x-ms-tracking-id,x-ms-workflow-id,etc.) adding to stringStr, which is not under our control. What's more, signature is only valid for 15m since generated.
There's a built-in Get Blob Metadata action. And for Storage, other common actions are available as well.
To set blob metadata, I suggest to host the logic in Azure function.
Follow this tutorial to create Function app and a httptrigger function, remember to choose the Storage Account where we need to set blob metadata.
Replace the httptrigger sample with code below, and modify metadataName to what you need.
#r "Microsoft.WindowsAzure.Storage"
using System.Net;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
dynamic data = await req.Content.ReadAsAsync<object>();
if (data == null)
{
return req.CreateResponse(HttpStatusCode.BadRequest, "No request body posted");
}
else
{
string metadata = data.metadata;
string blobName = data.blobName;
string containerName = data.containerName;
try
{
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("AzureWebJobsStorage"));
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer blobContainer = blobClient.GetContainerReference(containerName);
CloudBlob blob = blobContainer.GetBlobReference(blobName);
blob.Metadata.Add("metadataName", metadata);
blob.SetMetadata();
}
catch (Exception e)
{
log.Error(e.ToString());
return req.CreateResponse(HttpStatusCode.InternalServerError, "Fail to set metadata");
}
return (string.IsNullOrEmpty(metadata) || string.IsNullOrEmpty(blobName) || string.IsNullOrEmpty(containerName))
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass necessary parameters in the request body")
: req.CreateResponse(HttpStatusCode.OK, $"Metadata of {blobName} has been set");
}
}
In Microsoft Flow, create a Http action, post content below to the function url got in step 2.
{
"metadata": "test",
"blobName":"myblob",
"containerName":"mycontainer"
}
We can successfully acquire a token using the following code:
var certificate = Certificate.Load("Client.pfx", "notasecret");
var authenticationContext = new AuthenticationContext(authority);
var clientAssertionCertificate = new ClientAssertionCertificate(clientId, certificate);
return await authenticationContext.AcquireTokenAsync(resource, clientAssertionCertificate);
The token doesnt seem to contain any information that we can use to identity the client. In our use case we have lots of daemon service clients that communicate to a API. We need to have some unique identified available on the server.
I also tried creating our own JWT token and added some public claims, such as name. However after requesting client assertion type using the following code fragment
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "clientid", clientId },
{ "resource", resource },
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "grant_type", "client_credentials" },
{ "client_assertion", jwt }
});
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://login.windows.net/{guid}/")
};
var response = await httpClient.PostAsync("oauth2/token", content);
The return token had none of my custom information.
Question: Is there a way to pass custom claims using ClientAssertionCertificate flow? where the token returned has additional information.
There is currently no way of adding custom claims in tokens issued for applications.
The token you receive should contain the claims appid (which identifies the client_id of the application who requested the token) and tid (which indicates which azure AD tenant the app is operating on). Those two should be enough for you to identify the calling application. Now, if rather than the application you want to identify the process (as in, instance of application X running on server A and instance of application X running on server B) then I don't believe we have anything in Azure AD today that would help you to tell the two apart - for Azure AD if they have the same client_id and secret, they are the same application.