Note: This article uses Identity Domains and Identity Cloud Service (IDCS) interchangeably as the content is applicable to both services

POSIX (Portable Operating System Interface) attributes are things that you may recognize if you’ve ever glanced at the /etc/passwd file on a Linux machine. They are useful for letting the operating system know who is logging in, what shell to use, where the home directory is located, and whatever GECOS is being used for at the moment. If you are logging into your machine via the PAM Module for Linux, your user must have POSIX attributes to validate that you belong there and should be granted access. However, you will notice that there are no special UIs or tools to manage your POSIX attributes in OCI.

Why is this?

The simple answer is there hasn’t been a great demand for it. Most organizations have identity providers that were born during the day but not yesterday, and already have tools to manage these attributes in their primary systems. Instead of trying to re-invent the processes that they’re using in the primary identity provider, it’s much easier to just replicate the attributes to OCI and leave existing processes in place. Tools like the Microsoft Active Directory (AD) Bridge already exist for supporting replication from AD to OCI. However, there may be a technical or business reason that this scenario isn’t possible.

Using some form of replication from the primary Identity Provider to OCI is best practice, but if POSIX attributes need to be set and managed in OCI, the Identity Domain REST APIs can be used to manage user and group attributes. The REST APIs provide a full set of endpoints with which we can do any task with some effort. The code examples below are ways to interact with the APIs to solve the problem of assinging POSIX attributes to users. Reading the code below will give the gist of how to use the REST APIs with generic Python libraries.

Pre-Requisites

  • An Identity Domain/IDCS instance
  • Confidential Application with:
    • Client Credentials grant type
    • Client Access to Admin APIs — User Administrator or higher

The Code

Authentication

To authenticate to the Identity Domain REST APIs, the script will require access to three (3) sensitive pieces of data.

  1. The Identity Domain hostname (i.e. https://idcs-xyz.identity.oraclecloud.com)
  2. The Client ID of the Confidential Application
  3. The Client Secret of the Confidential Application

These need to be accessible to the application, but be sure not to hard code them. Best practice would be to use a service to manage access to these secrets, such as OCI Vault Secret Storage.

#!/bin/python3
import json
import logging

# OAuth
from oauthlib.oauth2 import BackendApplicationClient
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session

def get_token():
    """config contains a dict generated from a JSON file named IAMClientConfig.json:
       "iamurl": "https://idcs-xyz.identity.oraclecloud.com/"
       "client_id": "abc123"
       "client_secret": "abc-xyz"
       This data could also be obtained via environment variables via the os package
       Best practice for storing a retriving secrets should be used in place of this example
       These variables are good candidates for the OCI Vault service to store
       """     
    config = json.load(open('IAMClientConfig.json'))
    
    idcsURL = config["iamurl"]
    clientID = config["client_id"]
    clientSecret = config["client_secret"]
    
    logging.info("Initializing IDCS client with the following params:")
    logging.info("IAM URL: {}".format(idcsURL))
    logging.info("Client ID: {}".format(clientID))
    logging.info("Client Secret: {}".format(clientSecret))
    
    # Set up authentication
    auth = HTTPBasicAuth(clientID, clientSecret)
    client = BackendApplicationClient(client_id=clientID)
    oauthClient = OAuth2Session(client=client)
    
    #Aquire the token
        # NOTE: we don't actually need the access token ourselves
    #       The requests_oauthlib handles calling oauthlib to acquire that
    #       when it's needed. And as a bonus it also gets a new one if that
    #       expires.
    #
    # BUT: I go and get one here so that I know if the config settings are right
    #      before the other code tries to use the library for something.
    #
    # If acquiring the AT fails this code will throw an exception.
        
    token = oauthClient.fetch_token(token_url=idcsURL + '/oauth2/v1/token',
                                         auth=auth,
                                         scope=["urn:opc:idm:__myscopes__"]) # This scope is required
    logging.debug("Access Token: {}".format(token.get("access_token")))
    
    return token

This function returns a bearer token to be used with subsequent calls, but as the comments suggests, one could re-use the oauthClient object for future calls to the identity provider instead.

Sending Requests

Now that we can authenticate, the next step is to send data to the Identity Domain to perform operations. We will need to provide the Identity Domain URL as idcsUrl via a class attribute, global variable, or somewhere else within the scope of the function. This function will take an HTTP Request verb (POST, GET, PATCH, etc.), a URI (/admin/v1/Users), and a dict to be converted to JSON in the request body.

from requests_oauthlib import OAuth2Session

def _sendRequest(verb, uri, jsonpayload):
    """Send requests with authentication
    # Will need to provide oauthClient as type OAuth2Session
    # Will need to provide idcsUrl for IDCS host
    """
    if verb == "POST":
        logging.debug("Sending POST payload:")
        logging.debug(json.dumps(jsonpayload))

    # Use OAuth2Session to send request with authentication
    response = oauthClient.request(verb, idcsUrl + uri,
                                        json=jsonpayload,
                                        headers={
                                            "Content-Type": "application/scim+json",
                                            "Accept": "application/scim+json,application/json"
                                        })

    logging.debug("Status code: {}".format(response.status_code))

    if response.ok:
        logging.debug("Response indicates success")
        if response.content:
            logging.debug(response.content)
            if response.text:
                logging.debug(json.dumps(response.json()))
                # Return JSON response
                return response.json()
        else:
            return None
    else:
        # anything other than "OK" from IDCS means error
        logging.error("Error making HTTP request")
        if response.text:
            logging.debug(response.text)
        else:
            logging.debug("No content to log")

        raise Exception("HTTP request failed")

This allows us to send a generic request to the Identity Domain or IDCS instance in idcsUrl. Note that we are re-using the oauthClient for this example, but using the bearer token with a generic request library would work as well.

Finding Next Good UID

We can’t specify that the next UID number should be assigned to a user. The SCIM call needs to know which number to assign. To do this, we just have to send the correct query to the Identity Domain in order to find the highest UID currently in use. Then, we will use this data to assign the next highest number to the user. This way we can maintain the uniqueness of our UIDs without having to track state externally. The below shows how to construct a request to the REST API to retrieve the highest UID assigned to any user in the Identity Domain.

def getMaxUid(self):
     """query contains the URL query parameters to return the highest uidNumber currently assigned
     # filter -- Filter for users with uidNumber present (pr)
     # attributes -- Return uidNumber
     # sortBy -- Sort by uidNumber attribute
     # sortOrder -- Descending to start returning highest uidNumber first
     # count -- Return only 1 result, the highest uidNumber
     """

     query = {
         "filter": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:uidNumber pr",
         "attributes": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:uidNumber",
         "sortBy": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:uidNumber",
         "sortOrder": "descending",
         "count": "1"
     }

     # Combine Users endpoint with query
     uri = "/admin/v1/Users?" + urllib.parse.urlencode(query)

     response = _sendRequest("GET", uri, None)
     logging.debug("getMaxUid Response: {}".format(response))
     # Return only the UID ignoring the rest of the response
     return response["Resources"][0]["urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User"]["uidNumber"]

Bringing It Together

We finally have all the pieces to get POSIX attributes assigned to your OCI IAM users. We will need a way to take input and use it to make a SCIM call to the REST APIs. The Python standard libraries have a command line parser module called argparse. In this example we need two things from the script operator: a username to be looked up with the -u option and a GID to assign with the -gid option. Once those are read, we send a payload to get POSIX attributes assigned to the user provided by -u.

This script also uses a python class as a client to incorporate the functions and data we used above. This class is the IAMClient, and it is being used as an object oriented way to run the script functions. All you need to know about it is that it has the above functions as class methods to share some state between them.

#!/bin/python3
import argparse
import json
import logging

logging.basicConfig(format='%(asctime)s %(levelname)7s %(module)s:%(funcName)s -> %(message)s', level=logging.DEBUG)
logging.debug("Starting up")

# this is for our worker thread pool

parser = argparse.ArgumentParser()

parser.add_argument('-u', required=True, dest='username', help='IDCS Username')
parser.add_argument('-gid', required=True, type=int, dest='posix_gid', help='POSIX GID for user')

cmd = parser.parse_args()

# This example uses IAM Client code to communicate with IDCS
from IAMClient
import IAMClient
idcs = IAMClient()

# Getting the User ID from IDCS based on the input username
# Usernames and User IDs are never the same value
# This is not included in the sample code and must be added
userid = idcs.getUserId( cmd.username )

# Build the SCIM Payload
# Identity Domains/IDCS follow the SCIM 2.0 standards
# Payload will need to be tuned based on the type of operation needed
# This example is for adding attributes to a user that has no current values for these attributes
payload = \
    {
      "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:PatchOp"
      ],
      "Operations": [
        {
          "op": "add",
          "path": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:homeDirectory",
          "value": "/home/" + cmd.username # Append username to "/home" i.e. /home/jimsmith
        },
        {
          "op": "add",
          "path": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:gecos",
          "value": "Jim Smith 111-555-2222 Redwood Shores CA"
        },
        {
          "op": "add",
          "path": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:uidNumber",
          "value": (idcs.getMaxUid() + 1) # Get max UID and increment by 1
        },
        {
          "op": "add",
          "path": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:gidNumber",
          "value": cmd.posix_gid # GID value from command line
        },
        {
          "op": "add",
          "path": "urn:ietf:params:scim:schemas:oracle:idcs:extension:posix:User:loginShell",
          "value": "/bin/bash"
        }
      ]
    }

logging.debug("jSON payload is:")
logging.debug( json.dumps( payload, indent=4))

# Use _sendRequest to send payload
idcs._sendRequest( "PATCH", "/admin/v1/Users/" + userid, payload)

Wrapping Up

Using the examples above, it should be fairly easy to create a script for quickly assigning all attributes that one needs for authentication to Linux. This will allow you to manage attributes and user population via OCI IAM for a fleet of machines using the PAM Module. You can also extend based on your business needs. The full OCI Identity Domain/IDCS REST API documentation is available now.