Accessing Other Oracle Cloud Infrastructure Resources from Running Functions

Find out how to access other Oracle Cloud Infrastructure resources from running functions deployed to OCI Functions.

When a function you've deployed to OCI Functions is running, it can access other Oracle Cloud Infrastructure resources. For example:

  • You might want a function to get a list of VCNs from the Networking service.
  • You might want a function to read data from an Object Storage bucket, perform some operation on the data, and then write the modified data back to the Object Storage bucket.

To enable a function to access another Oracle Cloud Infrastructure resource, you have to include the function in a dynamic group, and then create a policy to grant the dynamic group access to that resource. For more information about dynamic groups, including the permissions required to create them, see Managing Dynamic Groups.

Having set up the policy and the dynamic group, you can then include a call to a 'resource principal provider' in your function code. The resource principal provider uses a resource provider session token (RPST) that enables the function to authenticate itself with other Oracle Cloud Infrastructure services. The token is only valid for the resources to which the dynamic group has been granted access.

Note also that the token is cached for 15 minutes. So if you change the policy or the dynamic group, you will have to wait for 15 minutes to see the effect of your changes.

We recommend that you use the resource principal provider included in the Oracle Cloud Infrastructure SDK. However, you might be writing a function in a language that the Oracle Cloud Infrastructure SDK does not support. Or you might simply not want to use the Oracle Cloud Infrastructure SDK. In either case, you can write your own custom resource principal provider to enable a function to authenticate itself with other Oracle Cloud Infrastructure services, using files and environment variables in the container in which the function is executing.

Using the Console

To enable a running function to access other Oracle Cloud Infrastructure resources:

  1. Log in to the Console and create a new dynamic group:

    1. Open the navigation menu and click Identity & Security. Under Identity, click Dynamic Groups.
    2. Follow the instructions in To create a dynamic group, and give the dynamic group a name (for example, acme-func-dyn-grp).
    3. When specifying a rule for the dynamic group, consider the following examples:

      • If you want all functions in a compartment to be able to access a resource, enter a rule similar to the following that adds all functions in the compartment with the specified compartment OCID to the dynamic group:

        ALL {resource.type = 'fnfunc', resource.compartment.id = 'ocid1.compartment.oc1..aaaaaaaa23______smwa'}
      • If you want a specific function to be able to access a resource, enter a rule similar to the following that adds the function with the specified OCID to the dynamic group:

        resource.id = 'ocid1.fnfunc.oc1.iad.aaaaaaaaacq______dnya'
      • If you want all functions with a specific defined tag to be able to access a resource, enter a rule similar to the following that adds all functions with the defined tag to the dynamic group :

        ALL {resource.type = 'fnfunc', tag.department.operations.value = '45'}

        Note that free-form tags are not supported. For more information about tagging, see Resource Tags.

    4. Click Create Dynamic Group.

    Having created a dynamic group that includes the function, you can now create a policy to give the dynamic group access to the required Oracle Cloud Infrastructure resource.

  2. Create a new policy:

    1. Open the navigation menu and click Identity & Security. Under Identity, click Policies.
    2. Follow the instructions in To create a policy, and give the policy a name (for example, acme-func-dyn-grp-policy).
    3. When specifying a policy statement, consider the following examples:

      • If you want functions in the acme-func-dyn-grp to be able to get a list of all the VCNs in the tenancy, enter a rule similar to the following:

        allow dynamic-group acme-func-dyn-grp to inspect vcns in tenancy
      • If you want functions in the acme-func-dyn-grp to be able to read and write to a particular Object Storage bucket, enter a rule similar to the following:

        allow dynamic-group acme-func-dyn-grp to manage objects in compartment acme-storage-compartment where all {target.bucket.name='acme-functions-bucket'}
      • If you want functions in the acme-func-dyn-grp to be able to read and write to all resources in a compartment, enter a rule similar to the following:

        allow dynamic-group acme-func-dyn-grp to manage all-resources in compartment acme-storage-compartment
    4. Click Create to create the new policy.
  3. Include a resource principal provider in the function code to enable the function to authenticate with other Oracle Cloud Infrastructure services. See:

    For a sample Java function, see Function that returns the list of instances in the calling Compartment in the OCI Functions samples repository on GitHub.

Example: Adding the Oracle Resource Principal Provider to a Python Function to Get a List of VCNs from the Networking Service

Having added a function to a dynamic group, and created a policy that allows the dynamic group to list the VCNs in the tenancy, you could include code similar to the following example to get a list of VCNs from the Networking service. This example uses the Oracle resource principal provider to extract credentials from the RPST token.

import io
import json

from fdk import response
import oci

def handler(ctx, data: io.BytesIO=None):
    signer = oci.auth.signers.get_resource_principals_signer()
    resp = do(signer)
    return response.Response(ctx,
        response_data=json.dumps(resp),
        headers={"Content-Type": "application/json"} )

def do(signer):
    # List VCNs --------------------------------------------------------
    client = oci.core.VirtualNetworkClient({}, signer=signer)
    try:
        vcns = client.list_vcns(signer.compartment_id)
        vcns = [[v.id, v.display_name] for v in vcns.data]
    except Exception as e:
        vcns = str(e)
    return {"vcns": vcns, }

Example: Adding a Custom Resource Principal Provider to a Function

We recommend that you use the resource principal provider included in the Oracle Cloud Infrastructure SDK. However, you might be writing a function in a language that the Oracle Cloud Infrastructure SDK does not support. Or you might simply not want to use the Oracle Cloud Infrastructure SDK. In either case, you can write your own custom resource principal provider to enable a function to authenticate itself with other Oracle Cloud Infrastructure services, using files and environment variables in the container in which the function is executing.

The container in which a function executes includes a directory tree that holds Oracle Cloud Infrastructure compatible credentials, specifically:

  • A resource principal session token (RPST) in a file named rpst. The RPST token is formatted as a JWT token, and includes claims that identify the function's host tenancy and compartment.
  • A private key for use in making requests to Oracle Cloud Infrastructure services on behalf of the function, in a file named private.pem.

The following environment variables are set inside the container in which the function executes:

  • OCI_RESOURCE_PRINCIPAL_VERSION, containing the value 2.2.
  • OCI_RESOURCE_PRINCIPAL_RPST, containing the absolute path to the rpst file (including the filename).
  • OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM, containing the absolute path to the private.pem file (including the filename).
  • OCI_RESOURCE_PRINCIPAL_REGION, containing the region identifier in which the function is deployed (for example, us-phoenix-1).

To enable a function to access another Oracle Cloud Infrastructure service, add code to the function so that it can authenticate itself with the other resource:

  1. Add code that loads the RPST token from the path in the OCI_RESOURCE_PRINCIPAL_RPST environment variable.
  2. Add code that loads the private key from the path in the OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM environment variable.

  3. Add code that uses the RPST token and the private key to create an Oracle Cloud Infrastructure request signature (see Request Signatures).

  4. Add code that constructs the request to the other Oracle Cloud Infrastructure resource.

    If necessary, you can identify:

    • The endpoints of other Oracle Cloud Infrastructure services in the same (local) region as the function, using the region identifier in the OCI_RESOURCE_PRINCIPAL_REGION environment variable.
    • The function's host tenancy and compartment, using the res_tenant and res_compartment claims in the RPST token.

For example, the sample Python function below includes a custom resource principal provider that extracts credentials from the RPST token. It then submits a GET request to the IAM API's getTenancy operation to return the OCID of the function's tenancy.

#!/usr/bin/env python3

import base64
import email.utils
import hashlib
import httpsig_cffi.sign
import json
import logging
import os.path
import re
import requests.auth
import urllib.parse


LOG = logging.getLogger(__name__)


# The following class is derived from the Python section in https://docs.cloud.oracle.com/iaas/Content/API/Concepts/signingrequests.htm

class SignedRequestAuth(requests.auth.AuthBase):
    """A requests auth instance that can be reused across requests"""
    generic_headers = [
        "date",
        "(request-target)",
        "host"
    ]
    body_headers = [
        "content-length",
        "content-type",
        "x-content-sha256",
    ]
    required_headers = {
        "get": generic_headers,
        "head": generic_headers,
        "delete": generic_headers,
        "put": generic_headers + body_headers,
        "post": generic_headers + body_headers,
    }

    def __init__(self, key_id, private_key):
        # Build a httpsig_cffi.requests_auth.HTTPSignatureAuth for each
        # HTTP method's required headers
        self.signers = {}
        for method, headers in self.required_headers.items():
            signer = httpsig_cffi.sign.HeaderSigner(
                key_id=key_id, secret=private_key,
                algorithm="rsa-sha256", headers=headers[:])
            use_host = "host" in headers
            self.signers[method] = (signer, use_host)

    def inject_missing_headers(self, request, sign_body):
        # Inject date, content-type, and host if missing
        request.headers.setdefault(
            "date", email.utils.formatdate(usegmt=True))
        request.headers.setdefault("content-type", "application/json")
        request.headers.setdefault(
            "host", urllib.parse.urlparse(request.url).netloc)

        # Requests with a body need to send content-type,
        # content-length, and x-content-sha256
        if sign_body:
            body = request.body or ""
            if "x-content-sha256" not in request.headers:
                m = hashlib.sha256(body.encode("utf-8"))
                base64digest = base64.b64encode(m.digest())
                base64string = base64digest.decode("utf-8")
                request.headers["x-content-sha256"] = base64string
            request.headers.setdefault("content-length", len(body))

    def __call__(self, request):
        verb = request.method.lower()
        # nothing to sign for options
        if verb == "options":
            return request
        signer, use_host = self.signers.get(verb, (None, None))
        if signer is None:
            raise ValueError(
                "Don't know how to sign request verb {}".format(verb))

        # Inject body headers for put/post requests, date for all requests
        sign_body = verb in ["put", "post"]
        self.inject_missing_headers(request, sign_body=sign_body)

        if use_host:
            host = urllib.parse.urlparse(request.url).netloc
        else:
            host = None

        signed_headers = signer.sign(
            request.headers, host=host,
            method=request.method, path=request.path_url)
        request.headers.update(signed_headers)
        return request


def rp_auther():
    if os.environ['OCI_RESOURCE_PRINCIPAL_VERSION'] != "2.2":
        raise EnvironmentError('{} must be set to the value "2.2"'.format('OCI_RESOURCE_PRINCIPAL_VERSION'))
    rpst = os.environ['OCI_RESOURCE_PRINCIPAL_RPST']
    if os.path.isabs(rpst):
        with open(rpst) as f:
            rpst = f.read()
    private_key = os.environ['OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM']
    if os.path.isabs(private_key):
        with open(private_key) as f:
            private_key = f.read()
    return get_claims(rpst), SignedRequestAuth('ST${}'.format(rpst), private_key)


def get_claims(rpst):
    """Parse an RPST as a JWT; return a dictionary of claims

    The claims that are important are: sub, res_compartment, and res_tenant.
    These carry the resource OCID together with its location.
    """
    s = rpst.split('.')[1]
    s += "=" * ((4 - len(s) % 4) % 4)  # Pad to a multiple of 4 characters
    return json.loads(base64.b64decode(s).decode('utf-8'))


# Use RP credentials to make a request
region = os.environ['OCI_RESOURCE_PRINCIPAL_REGION']
claims, rp_auth = rp_auther()

response = requests.get("https://identity.{}.oraclecloud.com/20160918/tenancies/{}".format(region, claims['res_tenant']), auth=rp_auth)
print(response.json())