Example of OCI Function as Resource Principal [enabled] to invoke OCI services

Lucas Jellema

Resource Principal is an OCI resource that through its membership of a Dynamic Group and permissions granted through policies to the Dynamic Group is given access to OCI resources and services. Examples of Resource Principals are Function and API Gateway.

A Function that is resource principal enabled can for example invoke the Object Storage Service API or the Vault Secret’s API or another Function. To do so, it does not need to use a human user’s private key. A private key is made available at run time by the OCI FaaS framework because of the fact that function is a resource principal.

image

In this article, I will demonstrate how this is done in the most straightforward way. The steps are:

  • create a Bucket on Storage Service; add an object to the Bucket
  • create a Dynamic Group with a rule to include Functions in a Compartment (or even functions with a specific name)
  • create a policy to grant this Dynamic Group the permission to read objects from the Bucket
  • create a Function with Node runtime
    • add OCI Node SDK NPM module to the function at the time of writing the OCI Node SDK library can not handle signing requests with the Private Key and RPST (Remote Principal Session Token) injected into the Resource Principal at run time; we have to rely on ‘manual request signing and explicit REST API requests’
    • have the function read the environment variables OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM and OCI_RESOURCE_PRINCIPAL_RPST
    • create the REST API request to read the file from Object Storage
    • sign the request using the Private Key and the RPST
    • make the request, handle the response and return the file contents as the function result

This image shows what the implementation will look like:

A bucket with an object that is read-enabled for a Dynamic Group through a policy. The composition of the Dynamic Group is defined through a rule that selects all functions in a specific compartment (lab-compartment). The function is implemented in Node, invokes the OCI REST API for the Object Storage Service. The REST request needs to be signed using a private key; this private key is injected by the OCI Functions runtime into the function because of its membership of the dynamic group which makes the function a resource principal.

image

Create Bucket and Object

Create Bucket function-resource-principal-test in Object Storage specifically for this article:

image

Then add a single file test-file.json to this bucket:

image

Create Dynamic Group that includes all Functions in the Compartment

Create Dynamic Group functions-in-lab-compartment. This group contains all functions in a specific compartment. This is achieved through the following matching rule:

resource.type = ‘fnfunc’, resource.compartment.id = ‘ocid1.compartment.oc1..aaaaaaaatxf2nfi7prglkhntadfj4tuxlfms36xhqc4hekuif6wjnoyq4ilq’

image

Define Policy to Grant Permission to Read Object in Bucket to Dynamic Group (Members)

Create a new policy that will define the permissions (through policy statements) that will be granted to the functions in the Dynamic Group. Set the name of the policy to something descriptive, such as oci-permissions-for-resource-principal-enabled-functions-in-lab-compartment.

image

Then define the policy statement that defines the permission for the dynamic group called functions-in-lab-compartment to read objects [through Object Storage Service] in the compartment lab-compartment and even more specifically in the bucket function-resource-principal-test:

allow dynamic-group functions-in-lab-compartment to read objects in compartment lab-compartment where all {target.bucket.name=’function-resource-principal-test’}

Create Function to Read Object

Because of the policy and the dynamic group, we know that if we create this new function in the lab-compartment it will become part of the dynamic group and inherit the permission to read objects from the bucket function-resource-principal-test. This function is – because of the dynamic group – Resource Principal [enabled]. This means we do not have to include a private key and the details for a specific OCI user in an oci-config file in the function. Instead, the function will have access to a system generated private key file as well as a Remote Principal Session Token (RPST). These files can be read from the function and are used to sign requests to the OCI REST API. These files can be found from the function using two environment variables that contain the file location for these two files.

Let’s first create a function implementation to check on these two environment variables and these files they refer to.

const fdk = require('@fnproject/fdk');
const fs = require('fs')

fdk.handle(async function (input) {
    const rpst = fs.readFileSync(process.env.OCI_RESOURCE_PRINCIPAL_RPST, { encoding: 'utf8' })
    const payload = rpst.split('.')[1]
    const payloadDecoded = Buffer.from(payload, 'base64').toString('ascii')
    const claims = JSON.parse(payloadDecoded)

  return {
     'pem-file': process.env.OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM
     ,'rpst-file' : process.env.OCI_RESOURCE_PRINCIPAL_RPST
     ,'rpst-claims' : claims
  }
})

When we deploy and invoke this function, we learn about the environment variables and the files injected by the OCI FaaS runtime framework because the Function is a Resource Principal as defined through the dynamic group it is a member of.

SNAGHTML1a981cf

From https://www.ateam-oracle.com/how-to-implement-an-oci-api-gateway-authorization-fn-in-nodejs-that-accesses-oci-resources, I find:

By looking at this RPST we see that it contains claims that fully describe our Fn resource. The sub claim is the OCID of the Authorization function. The res_tenant is the OCID of our tenancy. The ptype (principal type) claim is “resource”, indicating that this is a Resource Principal token, and you’ll notice that res_type is ‘fnfunc’, since our resource is a function. The jti claim is a replay prevention nonce. The jwk claim reference the JWKS set used to sign the token, including all the typical JWKS parameters like the ‘n’ key modulus, the ‘kty’ key type and ‘kid’ key id. The ‘ttype’ claim is res_sp, indicating it’s a resource principal session token, and notice the ‘opc-dgs’ claim. It references the OCID of the dynamic group which our function belongs to, and whose members we configured in our IAM policies to have read access privileges to the Object Storage bucket

Note: if the function is member of more than just one dynamic group, I wonder what will happen to the contents of the RPST. That is something for a later moment to investigate.

Now create the function again with code that actually creates and signs a request to the Object Storage service:

first a redefinition of func.js, which is nothing more than the wrapper for the function, handing over to readObject.js for the actual work:

const fdk = require('@fnproject/fdk');
const ro = require('./readObject')
fdk.handle(async function (input) {
    const r = await ro.readObject(input.namespace, input.bucketName, input.fileName)
    return {
     'fileContents': r
    }
})

The definition of readObject.js is shown here:

const https = require('https')
const httpSignature = require('http-signature')
const jsSHA = require("jssha")
const fs = require('fs')

function sign(request, options) {

    const headersToSign = [
        "host",
        "date",
        "(request-target)"
    ];

    const methodsThatRequireExtraHeaders = ["POST", "PUT"];

    if (methodsThatRequireExtraHeaders.indexOf(request.method.toUpperCase()) !== -1) {
        options.body = options.body || "";

        const shaObj = new jsSHA("SHA-256", "TEXT");
        shaObj.update(options.body);

        request.setHeader("Content-Length", options.body.length);
        request.setHeader("x-content-sha256", shaObj.getHash('B64'));

        headersToSign = headersToSign.concat([
            "content-type",
            "content-length",
            "x-content-sha256"
        ]);
    }

    httpSignature.sign(request, {
        key: options.privateKey,
        keyId: options.keyId,
        headers: headersToSign
    });

    const newAuthHeaderValue = request.getHeader("Authorization").replace("Signature ", "Signature version=\"1\",");
    request.setHeader("Authorization", newAuthHeaderValue);
}// sign

const readObjectFromStorage = async function (privateKey, keyId, tenancyId, namespace, bucketName, fileName) {
    /* return a promise that contains the REST API call */
    return new Promise((resolve, reject) => {
        /* the domain/path for the REST endpoint */
        const requestOptions = {
            host: 'objectstorage.us-ashburn-1.oraclecloud.com',
            path: `/n/${encodeURIComponent(namespace)}/b/${encodeURIComponent(bucketName)}/o/${encodeURIComponent(fileName)}`,
        };

        /* the request itself */
        const request = https.request(requestOptions, (res) => {
            let data = ''
            res.on('data', (chunk) => {
                data += chunk
            });
            res.on('end', () => {
                resolve(JSON.parse(data))
            });
            res.on('error', (e) => {
                reject(JSON.parse(e))
            });
        })

        /* sign the request using the private key, tenancy id and the keyId (see above) */
        sign(request, {
            privateKey: privateKey,
            tenancyId: tenancyId,
            keyId: keyId,
        })

        request.end()
    })
}//readObjectFromStorage

const readObject = async function (namespace, bucketName, fileName) {
    const privateKeyPath = process.env.OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM
    const sessionTokenFilePath = process.env.OCI_RESOURCE_PRINCIPAL_RPST
    const rpst = fs.readFileSync(sessionTokenFilePath, { encoding: 'utf8' })
    const privateKey = fs.readFileSync(privateKeyPath, 'ascii')

    const payload = rpst.split('.')[1]
    const buff = Buffer.from(payload, 'base64')
    const payloadDecoded = buff.toString('ascii')
    const claims = JSON.parse(payloadDecoded)

    /* get tenancy id from claims */
    const tenancyId = claims.res_tenant
    /*  set the keyId used to sign the request; the format here is the literal string 'ST$', followed by the entire contents of the RPST */
    const keyId = `ST$${rpst}`

    const response = await readObjectFromStorage(privateKey, keyId, tenancyId, namespace, bucketName,fileName)
    return response

}
module.exports = {
    readObject: readObject
}

A call to the function is constructed as follows:

export NAMESPACE=$(oci os ns get| jq -r  '.data')

echo -n "{\"namespace\":\"$NAMESPACE\", \"bucketName\":\"function-resource-principal-test\", \"fileName\": \"test-file.json\"}" | fn invoke "lab${LAB_ID}" secret-retriever --content-type application/json

image This demonstrates that our function – without adding a private key to the function – is able to invoke OCI REST APIs thanks to the runtime injection of a private key and RPST token as a result of the membership of the dynamic group of this function.  

Resources

Todd Sharpe:  Resource Principal Auth With Node.JS For Easy OCI REST API Access From Your Oracle Functions – https://blogs.oracle.com/developers/resource-principal-auth-with-nodejs-for-easy-oci-rest-api-access-from-your-oracle-functions Todd Sharp: OCI SDK For TypeScript Is Now Available – Here’s How To Use It In Your JavaScript Projects – https://blogs.oracle.com/developers/oci-sdk-for-typescript-is-now-available-heres-how-to-use-it A-Team Muhammad Abdel-Halim – How to Implement an OCI API Gateway Authorization Fn in Node.js that Accesses OCI Resources – https://www.ateam-oracle.com/how-to-implement-an-oci-api-gateway-authorization-fn-in-nodejs-that-accesses-oci-resources (Sign REST API Requests in NodeJS Functions using RPST and Private Key provided by OCI FaaS framework) NPM Package Oracle Cloud Infrastructure SDK for TypeScript and JavaScript  https://www.npmjs.com/package/oci-sdk , followed by the entire contents of the RPST */ const keyId = `ST${rpst}` const response = await readObjectFromStorage(privateKey, keyId, tenancyId, namespace, bucketName,fileName) return response } module.exports = { readObject: readObject } [/code] A call to the function is constructed as follows:

This demonstrates that our function – without adding a private key to the function – is able to invoke OCI REST APIs thanks to the runtime injection of a private key and RPST token as a result of the membership of the dynamic group of this function.

 

Resources

Todd Sharpe:  Resource Principal Auth With Node.JS For Easy OCI REST API Access From Your Oracle Functions – https://blogs.oracle.com/developers/resource-principal-auth-with-nodejs-for-easy-oci-rest-api-access-from-your-oracle-functions

Todd Sharp: OCI SDK For TypeScript Is Now Available – Here’s How To Use It In Your JavaScript Projects – https://blogs.oracle.com/developers/oci-sdk-for-typescript-is-now-available-heres-how-to-use-it

A-Team Muhammad Abdel-Halim – How to Implement an OCI API Gateway Authorization Fn in Node.js that Accesses OCI Resources – https://www.ateam-oracle.com/how-to-implement-an-oci-api-gateway-authorization-fn-in-nodejs-that-accesses-oci-resources (Sign REST API Requests in NodeJS Functions using RPST and Private Key provided by OCI FaaS framework)

NPM Package Oracle Cloud Infrastructure SDK for TypeScript and JavaScript  https://www.npmjs.com/package/oci-sdk

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Next Post

Connect Azure Pipelines with sonarcloud through maven (YAML)

Facebook0TwitterLinkedinIntroduction Sonarcloud is a static code analysis tool. This means that it checks out the code at your repository, does an analysis and shows you the results. It may look something like this: It gives detailed information about your code and you can for example let it scan if you […]