SAP CAP using cds.User attributes - odata

I authenticate on the server by feeding req.user with an instance of cds.User, and I add some attributes:
User {
id: '110226363079182595683',
attr: { name: 'depth1', email: 'depth1#protonmail.com' },
_roles: { 'identified-user': true, 'authenticated-user': true }
}
This allows me to call my CDS services with an authenticated user.
It works well.
Then, I have in my CDS schema an entity :
entity Comments {
key ID : Integer;
project : Association to Projects;
title : String;
text : String;
CreatedBy : String #cds.on.insert : $user;
CreatedByName : String #cds.on.insert : $user.name;
}
My schema on an SQLite database.
I start the server, I launch a UI5 application which allows to insert comments in OData V4 and here is what happens:
HTTP request:
--batch_id-1642708209182-45
Content-Type:application/http
Content-Transfer-Encoding:binary
POST Projects(1)/comments HTTP/1.1
Accept:application/json;odata.metadata=minimal;IEEE754Compatible=true
Accept-Language:fr-FR
Content-Type:application/json;charset=UTF-8;IEEE754Compatible=true
{"ID":0,"text":"test comment"}
--batch_id-1642708209182-45--
Group ID: $auto
Server Log:
[cds] - > CREATE Projects(1)/comments
HTTP Response:
--batch_id-1642708209182-45
content-type: application/http
content-transfer-encoding: binary
HTTP/1.1 201 Created
odata-version: 4.0
content-type: application/json;odata.metadata=minimal;IEEE754Compatible=true
location: Comments(101)
{"#odata.context":"../$metadata#Comments/$entity","ID":101,"project_ID":1,"title":null,"text":"test comment","CreatedBy":"110226363079182595683","CreatedByName":"depth1"}
--batch_id-1642708209182-45--
HTTP Request:
--batch_id-1642708209355-46
Content-Type:application/http
Content-Transfer-Encoding:binary
GET Projects(1)/comments(101)?$select=CreatedByName,ID,text HTTP/1.1
Accept:application/json;odata.metadata=minimal;IEEE754Compatible=true
Accept-Language:fr-FR
Content-Type:application/json;charset=UTF-8;IEEE754Compatible=true
--batch_id-1642708209355-46--
Group ID: $auto
Server log
[cds] - > READ Projects(1)/comments(101) { '$select': 'CreatedByName,ID,text' }
HTTP Response
--batch_id-1642708209355-46
content-type: application/http
content-transfer-encoding: binary
HTTP/1.1 200 OK
odata-version: 4.0
content-type: application/json;odata.metadata=minimal;IEEE754Compatible=true
{"#odata.context":"../$metadata#Comments(CreatedByName,ID,text)/$entity","CreatedByName":null,"ID":101,"text":"aaa"}
--batch_id-1642708209355-46--
In my Database, CreatedBy is filled but not CreatedByName.
Also in the Create request i got the CreatedByName filled by the server and returned it's really strange.
How can i insert some cds.User attributes to the database ?!
Thank you !

Oww
I found a solution ! By adding a behavior in service.js
const cds = require('#sap/cds')
module.exports = cds.service.impl(function() {
this.before('CREATE', 'Comments', fillData)
})
async function fillData(req) {
req.data.CreatedByName = req.user.attr.name;
}

Related

C# reading OData $batch multipart/mixed response to a meaningful object(s)

I am consuming an OData Service, I am successfully POSTing my request (using RestSharp) to /$batch endpoint and getting the response. the response header contains
"Content-Type" : "multipart/mixed; boundary=<GUID>"
Body is
--C4254E82B51CFE5BD04201606B9AB7C50
Content-Type: multipart/mixed; boundary=C4254E82B51CFE5BD04201606B9AB7C51
Content-Length: 2221
--C4254E82B51CFE5BD04201606B9AB7C51
Content-Type: application/http; charset=utf-8
Content-Length: 2037
content-transfer-encoding: binary
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 1732
location: https://test.api/Event/CarEntries('4003581738')
dataserviceversion: 2.0
etag: W/"datetimeoffset'2021-04-21T00%3A49%3A45Z'"
{ JSON }
--C4254E82B51CFE5BD04201606B9AB7C51--
--C4254E82B51CFE5BD04201606B9AB7C50
Content-Type: application/http; charset=utf-8
Content-Length: 15116
content-transfer-encoding: binary
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15017
dataserviceversion: 2.0
{ JSON }
--C4254E82B51CFE5BD04201606B9AB7C50--
How do I deserialise and extract the JSON Objects in my C# code? I do not want to invent a Regex pattern (well that is my last resort)
I did try using "Simple.OData.Client" (also a few others) but my request is not 100% compatible with the "Simple.OData.Client".
Also tried extracting using the below code but not necessary give me what I want
var sc = new StringContent(response.Content);
var content = sc.ReadAsStreamAsync().Result;
var streamContent = new StreamContent(content);
streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse(response.ContentType);
var provider = streamContent.ReadAsMultipartAsync().Result;
Can someone giveme the best way to extract the Json objects ?
Thanks
Nero
I manage to get this working HttpClient and System.Net.Http.Formatting.Extension
Below is the code
using (var httpClient = new HttpClient())
{
using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://service-url/Entity/v1.3/$batch"))
{
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Client-Id", "XXXXXXXXX");
// Add all the headers here
// this is your custom batch request
request.Content = new StringContent("--batch\ncontent-type: multipart/mixed;boundary=changeset\n\n--changeset\ncontent-type: application/http\nContent-Transfer-Encoding: binary\n\nPOST CarEntries HTTP/1.1\ncontent-type: application/json;charset=utf-8\naccept: application/json;\n\n{\n\"RefId\": \"Test\",\n\"Child\": {\n\"ChildId\": \"412000415\"\n}\n}\n--changeset--\n--batch--");
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed;boundary=batch"); // This is imporatnt - but please refer to your api documentation
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var multiPartContent = await response.Content.ReadAsMultipartAsync(); // This is part of the extension
var mixedContent = multiPartContent.Contents.First(); // you will have multiple contents, select the content you want
var data = await mixedContent.ReadAsStringAsync();// read it as string
Regex rg = new Regex(#"\{(.|\s)*\}"); // find the json object from mixed content
var json = rg.Match(data);
return JsonConvert.DeserializeObject<TMessage>(json.Value);
}
}
Hope this helps someone in the future. But still, my goal is to use "Simple.OData.Client" or "Microsoft.OData.Client"

Getting a 401 unauthorized error on One Login authentication grant type part 2 request

I am using MVC framework to make POST request to OneLogin API to get JWT. I am getting a 401 unauthorized message back at my PostAsync call in the code below.
Error look like following
StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Pragma: no-cache
Cache-Control: no-store, no-cache
Date: Wed, 17 Jun 2020 04:21:23 GMT
Set-Cookie: ol_oidc_canary_30=false; path=/; domain=.onelogin.com
X-Powered-By: Express
Content-Length: 77
Content-Type: application/json; charset=utf-8
}
Am i missing any parameters. I registered my localhost on One login dev account. Is there any setting there i need to update or change?
public async Task<OidcTokenResponse> ProcessToken(string code, string clientSec)
{
string authorityToken = OneLoginAuthorityToken;
var formData = new System.Net.Http.FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("client_id", OneLoginClientID),
new KeyValuePair<string, string>("client_secret", clientSec),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
});
using (var client = new System.Net.Http.HttpClient())
{
const SslProtocols _Tls12 = (SslProtocols)0x00000C00;
const System.Net.SecurityProtocolType Tls12 = (System.Net.SecurityProtocolType)_Tls12;
System.Net.ServicePointManager.SecurityProtocol = Tls12;
var res = await client.PostAsync(authorityToken, formData);
var json = await res.Content.ReadAsStringAsync();
var tokenReponse = Newtonsoft.Json.JsonConvert.DeserializeObject<OidcTokenResponse>(json);
return tokenReponse;
}
}
You need to send the redirect_uri again in this message - it is a security feature of the authorization code flow.
Also worth tracing the messages with a tool such as Fiddler to ensure that the messages sent over the wire are what you'd expect.
See steps 4 and 8 of my messages write up for something to compare against.

How to view content delivered from HttpResponseMessage (ASP.NET MVC Web Api)

I create json object and assign it to a StringContent of my HttpResponseMessage instance. Everything works fine when I call the Web API action, the result is 200, the content-length is how it should be, but how to find the content itself, where is the json? What I get in the browser and in Postman is this:
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StringContent, Headers:
{
Content-Type: application/json
}
Why is this instead of my json string?
Content: System.Net.Http.StringContent
If what you are trying to achieve is to return a valid JSON response, then this is the way to go in Asp.Net MVC
public ActionResult HttpResponseMessage()
{
var oJSON = new { url = "path_to_file", hash = "aaaaaaaaaaaaaaaaa" };
return Json(oJSON, JsonRequestBehavior.AllowGet);
}
Response headers as seen by Postman:
Cache-Control →private
Content-Length →49
Content-Type →application/json; charset=utf-8
Date →Fri, 26 Oct 2018 13:31:44 GMT
Server →Microsoft-IIS/10.0
X-AspNet-Version →4.0.30319
X-AspNetMvc-Version →5.2
X-Powered-By →ASP.NET
X-SourceFiles →=?UTF-8?B?RTpcRXhhbSA3MCA0ODdcNzA0ODdcTVZDUm91dGVzXEhvbWVcSHR0cFJlc3BvbnNlTWVzc2FnZQ==?=
Response body as seen by Postman
{"url":"path_to_file","hash":"aaaaaaaaaaaaaaaaa"}

Create annotation to a contact entity in Microsoft Dynamics CRM by API

This question is related to Microsoft Dynamics CRM 2015, that I'm calling through API.
I create contact entity:
POST [organization URI]/api/data/contacts
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"emailaddress1": "myemail#example.com",
}
It works, I see new record, after I log into the panel.
And I can call it through the API:
[organization URI]/api/data/contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)
{
"#odata.context":"[organization URI]/api/data/$metadata#contacts/$entity",
"#odata.etag":"W/\"460199\"",
...
"contactid":"f76e4e7c-ea61-e511-80fd-3863bb342b00",
"emailaddress1":"myemail#example.com",
....
}
Next thing I want to do, is to add annotation record associated with that contact.
Following the guide I call:
POST [organization URI]/api/data/annotations
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"notetext": "TEST",
'contact#odata.bind': 'contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)'
}
But it returns 400 error:
An undeclared property 'contact' which only has property annotations in the payload but no property value was found in the payload. In OData, only declared navigation properties and declared named streams can be represented as properties without values.
When I call:
POST [organization URI]/api/data/annotations
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"notetext": "TEST",
}
New entity is created, but without a relation to contact.
How to properly compose this POST request? What am I missing here?
I suspect, that contact#odata.bind should be presented somehow different, I've tried contactid#odata.bind, object#odata.bind, objectid#odata.bind - but no effects.
Any ideas?
Instead of using objectid#odata.bind, you have to use objectid_contact#odata.bind. This results are in:
"objectid_contact#odata.bind": "/contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)"
To get the list of properties, look under the single-valued navigation properties in the documentation.
Part 1:
MS Docs Reference: Deep Insert
You can create entities related to each other by defining them as navigation properties values. This is known as deep insert.
As with a basic create, the response OData-EntityId header contains the Uri of the created entity. The URIs for the related entities created aren’t returned.
Below code is to create Account (1), create + associate Primary contact (2), create & Associate Opportunity (3) and create + associate Task (4)
POST [Organization URI]/api/data/v8.2/accounts HTTP/1.1
Content-Type: application/json; charset=utf-8
OData-MaxVersion: 4.0
OData-Version: 4.0
Accept: application/json
{
"name": "Sample Account",
"primarycontactid":
{
"firstname": "John",
"lastname": "Smith"
},
"opportunity_customer_accounts":
[
{
"name": "Opportunity associated to Sample Account",
"Opportunity_Tasks":
[
{ "subject": "Task associated to opportunity" }
]
}
]
}
Part 2:
Associating annotation to contact uses the below syntax.
note["objectid_contact#odata.bind"] = "/contacts(C5DDA727-B375-E611-80C8-00155D00083F)";
Refer SO link & blog
Part 3:
Answer to your comment on another answer about annotation_id_from_first_request:
To get the created record Id in response from last request, you can parse like below:
//get Response from Created Record
entityIdWithLink = XMLHttpRequest.getResponseHeader("OData-EntityId");
//get EntityId from ResponseHeader of Created Record
getEntityId = entityIdWithLink.split(/[()]/);
getEntityId = getEntityId[1];
You can read more
You can compose your POST request so that data from the created record will be returned with a status of 201 (Created).
To get this result, you must use the return=representation preference in the request headers.
To control which properties are returned, append the $select query option to the URL to the entity set.
The $expand query option will be ignored if used.
When an entity is created in this way the OData-EntityId header containing the URI to the created record is not returned
Note: This capability was added with December 2016 update for Dynamics 365
MS Docs Reference: Create with data returned
Update:
If anyone looking for working payload sample to deep insert a record + annotation, the below is from my project:
data = {
"new_attribute1": "test attribute 1",
"new_attribute2": "test attribute 2",
"new_comments": "test comments",
"new_recordurl": recordURL,
"new_feedback_Annotations":
[
{
"notetext": "Screenshot attached",
"subject": "Attachment",
"filename": file.name,
"mimetype": file.type,
"documentbody": base64str,
}
]
};
I've found this working, but in two requests:
POST [organization URI]/api/data/annotations
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"notetext": "TEST"
}
POST [organization URI]/api/data/contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)/Contact_Annotation/$ref
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"#odata.id": "[organization URI]/annotations(annotation_id_from_first_request)"
}
Edit:
annotation_id_from_first_request value is taken form first request's response.
This answer applies for web api usage:
If the references property has been defined using uppercase letters, you have to use uppercase letters in the property on update and insert. Look at the Schema name in the property list of the primary entity.
Lets say you have an entity called myprefix_entity with a reference to the account entity, and you named it Account, and the schema name became myprefix_AccountId, you would have to refer it as:
"myprefix_AccountId#odata.bind":"/accounts(f76e4e7c-ea61-e511-80fd-000000000000)"
The uppercase A and the uppercase I in myprefix_AccountId matters, if that is how the schema name has been defined.
I'm using this C# Code for creating and linking (the Task.Await stuff is not very clever, so... be careful):
dynamic testAno = new ExpandoObject();
testAno.NoteText = "Hello World!";
testAno.Subject = "Note Subject";
dynamic refAccount = new ExpandoObject();
refAccount.LogicalName = "account";
refAccount.Id = "003CCFC2-4012-DE11-9654-001F2964595C";
testAno.ObjectId = refAccount;
testAno.ObjectTypeCode = refAccount.LogicalName;
var demo = JsonConvert.SerializeObject(testAno);
HttpContent content = new StringContent(demo, Encoding.UTF8, "application/json");
var handler = new HttpClientHandler { UseDefaultCredentials = true };
HttpClient client = new HttpClient(handler);
var test = client.PostAsync(new Uri("http://crm/.../XRMServices/2011/OrganizationData.svc/AnnotationSet"), content).Result;
The JSON is looking like this:
{"NoteText":"Hello World!",
"Subject":"Note Subject",
"ObjectId": {"LogicalName":"account",
"Id":"003CCFC2-4012-DE11-9654-001F2964595C"}
,"ObjectTypeCode":"account"}
You can use following.
'contactid_contact#odata.bind': '/contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)'
In most of the record you will get _contactid_value as a parameter name. So you have to pass like contactid_entityname#odata.bind as a parameter and in the value you have to pass 'EntitySetName' which would be contacts and GUID. '/EntitysetName(GUID)'
So the value will be '/contacts(f76e4e7c-ea61-e511-80fd-3863bb342b00)'
might be a bit late for this, but the answer in the following link explains how the binding works really well.
basically, you need to use the field schema name with the suffix #odata.bind and the value being "/entityschemaname(recordGUID)" good to remember that the entityschemaname needs to have an 's' and the recordGUID should not have the curly brackets.
for more information follow this link below where i got this information from
'An undeclared property' when trying to create record via Web API
If you use OData, then you can do it this way...
In your Annotation data model:
[DataContract(Name = "annotations")]
public record Annotation
{
[DataMember(Name = "objectid_rd_servicerequestsession")]
public ServiceRequestSession ObjectId { get; set; } = default!;
[DataMember(Name = "objecttypecode")]
public string ObjectTypeCode { get; set; } = default!;
Where the rd_servicerequestsession is your entity name. Then you just need to create a new object Annotation object
var annotation = new Annotation
{
ObjectId = serviceRequestSession,
ObjectTypeCode = "rd_servicerequestsession",
And simply invoke InsertEntry method.

dotnetopenauth twitter api 1.1 signed request

I stuсk on using DNOA library for twitter 1.1 api
enter code here
I am trying to call users/show.json api
protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
{
string accessToken = response.AccessToken;
string str2 = response.ExtraData["user_id"];
string userName = response.ExtraData["screen_name"];
Uri location = new Uri("https://api.twitter.com/1.1/users/show.json?user_id=" + str2);
MessageReceivingEndpoint profileEndpoint = new MessageReceivingEndpoint(location, HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
HttpWebRequest request = base.WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken);
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("accesstoken", accessToken);
try
{
using (WebResponse wresponse = request.GetResponse())
{
var str = Utilities.ProcessResponse(wresponse);
var json = JObject.Parse(str);
dictionary.AddNotEmpty("name", json.Value<string>("name"));
dictionary.AddNotEmpty("location", json.Value<string>("location"));
dictionary.AddNotEmpty("description", json.Value<string>("description"));
dictionary.AddNotEmpty("url", json.Value<string>("url"));
}
}
catch (Exception)
{
}
return new AuthenticationResult(true, base.ProviderName, str2, userName, dictionary);
}
This what is sends to twitter
GET https://api.twitter.com/1.1/users/show.json?user_id=2193937074 HTTP/1.1
Authorization: OAuth oauth_token="2193937074-cgmZbmJIIb75f7MkQgbdjuvQaen2xzM1WFXXC7G",oauth_consumer_key="XVCgN3fkwzTGgeSm1FBa1Q",oauth_nonce="93UjjRkP",oauth_signature_method="HMAC-SHA1",oauth_signature="YzfXzU3VeEI9xl2SfuknPB33%2FiM%3D",oauth_version="1.0",oauth_timestamp="1389265955"
Host: api.twitter.com
The responce is
HTTP/1.1 401 Unauthorized
content-length: 63
content-type: application/json; charset=utf-8
date: Thu, 09 Jan 2014 11:12:36 UTC
server: tfe
set-cookie: guest_id=v1%3A138926595613849064; Domain=.twitter.com; Path=/; Expires=Sat, 09-Jan-2016 11:12:36 UTC
strict-transport-security: max-age=631138519
{"errors":[{"message":"Could not authenticate you","code":32}]}
The dev.twitter's OAuth tool shows the valid sample of signed header:
GET https://api.twitter.com/1.1/users/show.json?user_id=2193937074 HTTP/1.1
Authorization: OAuth oauth_consumer_key="XVCgN3fkwzTGgeSm1FBa1Q", oauth_nonce="dbf6f6c1aa6dc226de25265da3d63167", oauth_signature="K3Qfyc9qANFgckQNyqsaDWCnh%2BY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1389266681", oauth_token="2193937074-cgmZbmJIIb75f7MkQgbdjuvQaen2xzM1WFXXC7G", oauth_version="1.0"
Host: api.twitter.com
It loook's like the main difference is in length of the oauth_nonce?
DNOA - oauth_nonce="93UjjRkP"
OAuth tool - oauth_nonce="dbf6f6c1aa6dc226de25265da3d63167"
I solved the problem.
The main problem is how the signature is created, the TokenSecret is excluded from it's forming. The core of the this behavior is the AuthenticationOnlyCookieOAuthTokenManager manager that is used inside the base DotNetOpenAuth.AspNet.Clients.TwitterClient class.
public class AuthenticationOnlyCookieOAuthTokenManager : IOAuthTokenManager
{
...
public virtual void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
{
HttpCookie cookie = new HttpCookie("OAuthTokenSecret") {
Value = string.Empty, //<<< now it's empty
Expires = DateTime.UtcNow.AddDays(-5.0)
};
this.Context.Response.Cookies.Set(cookie);
}
...
}
It's just remove the tokenSecret;
The solution is to use the DotNetOpenAuth.AspNet.Clients.InMemoryOAuthTokenManager class. So you need just derive from OAuthClient and implement proper constructor:
public class TwitterClient : DotNetOpenAuth.AspNet.Clients.OAuthClient
{
protected TwitterClient(string appKey, string appSecret) :
base ("twitter",
new DotNetOpenAuthWebConsumer(
TwitterServiceDescription,
new InMemoryOAuthTokenManager(appKey, appSecret)))
{ }
...
}
Also have found the familiar post Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secret

Resources