Forum Discussion

jeffshurtliff's avatar
4 years ago

Unable to publish message on behalf of another user

I have created a custom endpoint wherein a new TKB article gets published to one of our product boards as a product advisory, which is triggered by a member of the respective product team via a custom component on an "administrative" page that I've developed.

Everything works as expected in terms of creating the message, but one of the requirements I'm attempting is for the content to be published using a generic "ProductTeam" service account, which I can't seem to get working properly.  

Even when passing a valid session key for the service account in the header or as a query parameter, the message is still published by the user (i.e. me) that triggers the endpoint.

These are the two functions I wrote to handle the publishing:

<#-------------------- Function: getSessionKey -------------------->
<#-- This function attempts to authenticate as a user and return the session key -->
<#function getSessionKey userLogin userPw>
  <#local sessionKey = "" />
  <#local queryString = "user.login=${userLogin?url}&user.password=${userPw?url}&restapi.response_format=json" />
  <#attempt>
    <#local response = restadmin('/authentication/sessions/login?${queryString}') />
  <#recover>
    <@logging.consoleError "Failed to authenticate as the user" />
  </#attempt>
  <#if response?? && response.@status == "success">
    <#local sessionKey = response.value />
  <#else>
    <@logging.consoleError "Authentication request for the user was not successful" />
  </#if>
  <#return sessionKey />
</#function>

<#-------------------- Function: publishNewMessage -------------------->
<#-- This function publishes a new message and returns the API response -->
<#function publishNewMessage payload sessionKey="" sessionKeyInQuery=false>
  <#if sessionKey?? && sessionKey?length gt 0>
    <#if sessionKeyInQuery?? && sessionKeyInQuery>
      <#local messagePostCall = restBuilder()
        .method("POST")
        .path("/messages")
        .queryParam("restapi.session_key", "${sessionKey}")
        .body(payload)
        .admin(false) />
    <#else>
      <#local messagePostCall = restBuilder()
        .method("POST")
        .path("/messages")
        .header("li-api-session-key","${sessionKey}")
        .body(payload)
        .admin(false) />
    </#if>
  <#else>
    <#local messagePostCall = restBuilder()
      .method("POST")
      .path("/messages")
      .body(payload)
      .admin(true) />
  </#if>
  <#local response = messagePostCall.call() />
  <#return response />
</#function>

 

Here are some usage examples, both of which still publish the message with my own user as the author rather than the service account, despite passing its session key.

<#assign payload = {
  "data": {
    "type": "message",
    "board": {
      "id": "some-product-board"
    },
    "subject": "This is a product advisory",
    "body": "This is the body of the message"
  }
} />
<#assign sessionKey = getSessionKey(SVC_USERNAME, SVC_PWD) />
<#-- Passing the session key in the header -->
<#assign response = publishNewMessage(payload, sessionKey) />
<#-- Passing the session key in the query string -->
<#assign response = publishNewMessage(payload, sessionKey, true) />

Does anyone have any ideas regarding what I might be doing wrong?  Thanks!

  • DougS's avatar
    DougS
    4 years ago

     

    Hi jeffshurtliff ,

    Let me reply to your last 2 questions:


    Is there a way to force the directives above to select one session key over another, or is there perhaps a different directive/method to leverage instead to publish the post within the endpoint?


    There is no way (to my knowledge) to pass a different session key from your freemarker template and have that override the session id of the currently authenticated user. We do allow you to pass either the Li-Api-Session-Key header or the restapi.session_key parameter to your endpoint call and it will use that session key to authenticate the user.

     


    Is there maybe a different way that the author can be specified by the Community APIs when creating a new message?  For example, if I pass "author": {"type": "user", "id": "12345"} in the payload when creating the message will that specify the author or will it be ignored?

    If the user you are authenticated as has the "Switch User" permission, then you can pass the "author": { "type": "user", "id": "12345" } and it should set the user for the id you've passed as message author (assuming that id maps to a valid user) instead of using the currently authenticated user as the author.

    I hope that helps.

    -Doug

3 Replies

  • DougS's avatar
    DougS
    Khoros Oracle
    4 years ago

    Hi jeffshurtliff,

    For security reasons, I would advise against trying to take the username and password as part of this endpoint. Any endpoint can be passed a REST V1 Session key (just add a request parameter named restapi.session_key=<thesessionkey>), so it's ideal to have the caller call /authentication/sessions/login endpoint and then pass the REST V1 Session key that gets returned with any endpoint that needs authentication.

    So you'd first want to make a call like the following to the /authentication/sessions/login endpoint to get a REST V1 Session Key:

    curl --location --request POST 'https://community.some.com/restapi/vc/authentication/sessions/login?restapi.response_format=json' \
    --form 'user.login="yourlogin"' \
    --form 'user.password="yourpassword"'

    That should return a response like this:

    {
      "response": {
        "status": "success",
        "value": {
          "type": "string",
          "$": "XYSopedCuqLQT179LjNxyFgTSne5vdLUb1MMgxmcSGQ."
        }
      }
    }

    Then you would call your endpoint, passing the session key returned from the /authentication/sessions/login call to the endpoint:

    curl --location --request POST 'https://community.some.com/plugins/custom/yourcustomername/yourcommuntiyname/messagepostingendpoint' \
    --header 'Content-Type: application/json' \
    --header 'Li-Api-Session-Key: XYSopedCuqLQT179LjNxyFgTSne5vdLUb1MMgxmcSGQ.'

    Also, I would advise against adding the .admin(true) option at all, as this could give an attacker a way to flood your community with message posts.

    I think you also have some spaces in between your builder method calls that might be causing an issue as well.

    I would recommend using a function like this to do the message posts:

    <#-------------------- Function: publishNewMessage -------------------->
    <#-- This function publishes a new message and returns the API response -->
    <#function publishNewMessage payload>
      <#if user.anonymous>
        <#return {
            "status" : "error",
            "message" : "You are not authorized to make this request",
            "data": {
              "type": "error_data",
              "code": 224,
              "developer_message": "",
              "more_info": ""
            }
          } />
      <#else>
        <#local messagePostCall = restBuilder()
          .method("POST")
          .path("/messages")
          .body(payload)
          .admin(false) />
        <#local response = messagePostCall.call() />
        <#return response />
      </#if>
    </#function>

    Here is a usage example, which includes a macro for rendering out an error response (assuming your endpoint is using application/json for its content type):

    <#-------------------- Macro: errorResponse -------------------->
    <#-- This macro returns a REST V2 style JSON response for errors -->
    <#macro errorResponse error>
      {
        "status": "error",
        "message": "${error.message}",
        "data": {
          "type": "error_data",
          "code": ${error.data.code},
          "developer_message": "${error.data.developer_message}",
          "more_info": "${error.data.more_info}"
        }
      }
    </#macro>
    
    <#assign payload = {
      "data": {
        "type": "message",
        "board": {
          "id": "Otis"
        },
        "subject": "This is a product advisory",
        "body": "This is the body of the message"
      }
    } />
    <#assign response = publishNewMessage(payload) />
    <#if response.status == "error">
      <@errorResponse error=response />
    <#else>
      ${apiv2.toJson(response)}
    </#if>

    I hope that helps!

    -Doug

  • Hi DougS,

    Thank you for the detailed response in this thread, and particularly around your suggestions regarding error reporting macros, which is super useful.

    Unfortunately I think you may have misinterpreted the primary ask and a few details in my original post:

    • What I'm ultimately trying to do is "ghostwrite" a message for another author.  For example, allowing the admin user JohnDoe to publish a TKB article that shows the author as JaneDoe rather than himself.  (To be even more specific, we want to publish release notes and other content under a generic "ProductTeam" user account rather than our product management team having to publish content under their own names.)
    • I am not having credentials passed at all via custom endpoint, and instead the getSessionKey function referenced above is a private function called within the endpoint just to generate a valid session key to use in the "ghostwritten" message creation.
    • The functions above are not the entire custom endpoint but are just small fragments of it.  The full endpoint can only be executed by an authenticated user belonging to specific roles, leverages CSRF tokens, etc. to prevent unauthorized API calls, and as mentioned does not request any credentials or other sensitive data over the wire.
    • The primary issue with which I need help is that whenever I use the restrestadmin or restBuilder directives and supply a valid session key in the header or as a query parameter, the provided session key is always trumped by my own and the messages are still always show me (or whoever calls the endpoint) as the message author rather than the account associated with the supplied session key.
      • Is there a way to force the directives above to select one session key over another, or is there perhaps a different directive/method to leverage instead to publish the post within the endpoint?
      • Is there maybe a different way that the author can be specified by the Community APIs when creating a new message?  For example, if I pass "author": {"type": "user", "id": "12345"} in the payload when creating the message will that specify the author or will it be ignored?

     

    I hope this helps to clarify things.

    Thanks again!

  • DougS's avatar
    DougS
    Khoros Oracle
    4 years ago

     

    Hi jeffshurtliff ,

    Let me reply to your last 2 questions:


    Is there a way to force the directives above to select one session key over another, or is there perhaps a different directive/method to leverage instead to publish the post within the endpoint?


    There is no way (to my knowledge) to pass a different session key from your freemarker template and have that override the session id of the currently authenticated user. We do allow you to pass either the Li-Api-Session-Key header or the restapi.session_key parameter to your endpoint call and it will use that session key to authenticate the user.

     


    Is there maybe a different way that the author can be specified by the Community APIs when creating a new message?  For example, if I pass "author": {"type": "user", "id": "12345"} in the payload when creating the message will that specify the author or will it be ignored?

    If the user you are authenticated as has the "Switch User" permission, then you can pass the "author": { "type": "user", "id": "12345" } and it should set the user for the id you've passed as message author (assuming that id maps to a valid user) instead of using the currently authenticated user as the author.

    I hope that helps.

    -Doug