Skip to main content
Version: v5.6.1

The PingOne Advanced Identity Cloud pack (packs:ping-aic)

packs:ping-aic

The packs:ping-aic entitlement gates opschain-pack-ping-aic, the signed commercial content pack for PingOne Advanced Identity Cloud (formerly ForgeRock Identity Cloud): a tenant's configuration as YAML resources your repository commits, reconciled by ping_aic:plan / apply / export, and the tenant operations a governed change composes — environment promotion, environment secrets and variables, identities, service accounts, tenant logs. The pack is hand-typed Go over the tenant's REST APIs via its own pingaic tool — no SDK, no vendor CLI, no interpreter — executed by the runner, with outbound HTTPS from the runner to the tenant as the only wire. The pack's signed manifest names requires: packs:ping-aic, so the entitlement in your licence must spell exactly that; like every packs:<name> grant it is validated by shape and minted per customer (see entitlements).

The pack lives in the opschain-pack-ping family repository and mounts like any other action pack — a packs: entry in the template's .opschain/packs.yaml naming the pack remote, the ping-aic path and a pinned revision (opschain docs getting-started/2-4-using-packs) — or installs into the pack store as a signed bundle. A mount name is an action namespace and may carry only lowercase letters, digits and underscores, so the pack mounts as ping_aic and its actions read ping_aic:plan, ping_aic:promotion_promote, and so on. Both doors enforce the manifest: installing a signed bundle verifies its signature and its required entitlement, and running any ping_aic:* action without packs:ping-aic refuses with the named pack refusal before anything dials the tenant.

The tenant and its identity​

Every action targets a ping_tenant document — the tenant FQDN, its environment tier (dev, staging or prod), the realms the declarative resources cover, the environment variables the service account arrives in, and whether the tenant opts into the gated api lane:

# .opschain/ping/tenant.yaml
kind: ping_tenant
name: acme-dev
spec:
tenant: openam-acme-dev.forgeblocks.com
tier: dev
realms: [alpha]
service_account:
id_env: PING_AIC_SA_ID
private_key_env: PING_AIC_SA_PRIVATE_KEY
api_lane: disabled

Commit one document per tenant environment and point each OpsChain environment at its own through the ping_aic.tenant_file property. Tenant environments then map onto OpsChain environments: path-based authorisation governs who may touch production, and an approval policy on the environment governs the promotion into it — the pack never invents an approval of its own.

The pack dials the tenant as a service account. Its id and private key (JWK or PEM) reach the step as environment the platform resolved from vault references — opschain.env properties on the OpsChain environment (opschain docs getting-started/3-5-secrets-and-the-vault):

{"opschain": {"env": {
"PING_AIC_SA_ID": "secret-vault://ping/dev/sa_id",
"PING_AIC_SA_PRIVATE_KEY": "secret-vault://ping/dev/sa_private_key"
}}}

Per step the tool mints a JWT-bearer assertion, exchanges it for a short-lived access token and keeps the token in memory only. Each dial logs one value-free line naming which credential variables were present (kind and length only); a variable still carrying an unresolved vault reference refuses the dial by name. Tenant logs authenticate with the log API key pair the same way (PING_AIC_LOG_API_KEY / PING_AIC_LOG_API_SECRET).

Declarative resources​

A resource is one YAML document — kind, name, realm for realm-scoped kinds, and spec, the tenant object's own JSON expressed in YAML. Ten kinds, in the order apply writes them:

KindScopeTenant object
ping_aic_esvtenantan environment variable (type: variable) or secret (type: secret — the value only ever comes from value_env, a vault-resolved environment variable, and is never read back)
ping_aic_scriptrealma script; script is plain source in YAML and base64 on the wire; _id pins the identity
ping_aic_journeyrealman authentication journey with every node it references — spec.tree plus spec.nodes — as one promotable document
ping_aic_oauth2_clientrealman OAuth 2.0 client
ping_aic_servicerealma realm service's top-level configuration
ping_aic_managed_objecttenantone object of the managed-object configuration
ping_aic_connectortenanta connector (provisioner) configuration
ping_aic_sync_mappingtenantone synchronisation mapping
ping_aic_email_templatetenantan e-mail template
ping_aic_themerealmone hosted-pages theme

The semantics are what make the resources safe to commit and re-apply:

  • A document owns the top-level keys it declares. plan compares those keys with the live object (volatile fields stripped, encrypted values reduced to a marker) and an update sends the live object overlaid with the document, so undeclared keys keep their tenant values and a partial document stays idempotent across plans.
  • ${values.<key>} placeholders take per-environment values from the values input — a JSON object or @file:<path>, defaulting to the environment's ping_aic.values_file property — so one resource set serves dev, staging and prod; a placeholder with no value fails by name.
  • Secrets never sit in YAML: an ESV secret names value_env; exported secrets carry no value; connector credentials are best declared as ESV placeholders the tenant resolves.
  • Deletes are opt-in per kind (prune: ping_aic_journey,ping_aic_script) and scoped to the realms the documents cover.
  • Export is deterministic — <kind>/<realm>/<name>.yaml (_global for tenant-wide kinds), sorted keys — so a repeat export of an unchanged tenant is byte-identical and an export followed by a plan is a no-op: adopting an existing tenant is an export, a review and a commit.

The shipped actions​

Every action takes tenant_file (defaulting to the ping_aic.tenant_file property, else .opschain/ping/tenant.yaml) and publishes its outputs with outputs: passthrough.

  • ping_aic:plan / ping_aic:apply / ping_aic:export — the declarative lane: dir (default .opschain/ping/resources), values, prune, and the rendered plan in plan_file (plus plan_json) as the change's evidence; outputs creates, updates, deletes, unchanged, has_changes, and after apply created / updated / deleted. Export takes kinds and realms.
  • ping_aic:config_export / config_import / config_diff / adopt — the configuration tree: one JSON object per file, global and per-realm, in the layout ForgeRock's sample configuration-manager tooling pulls, as the pack's import and adoption format. A repository already holding a tenant's configuration that way moves under OpsChain governance without conversion; adopt converts a tree into YAML resources and reports directories outside the layout in unmapped, never dropping them silently. config_diff is the drift probe: has_changes tells a workflow whether the committed tree and the tenant differ, and plan_file carries the diff.
  • ping_aic:promotion_lock / promotion_status / promotion_promote / promotion_rollback / promotion_unlock — dialled on the source (lower) environment's tenant: dev promotes into staging, staging into prod, and the step log announces the tiers. promotion_promote takes description, dry_run, unlock_after, wait (default on) and wait_timeout, and lands the tenant's promotion report in report_file; promotion_lock publishes the promotion_id that promotion_unlock releases; promotion_status is the wait predicate.
  • ping_aic:esv_set / esv_list / esv_delete / restart / restart_status — environment secrets and variables. A variable's value may be an input; a secret's value always comes from value_env. esv_list publishes names only. restart applies ESV changes and, with wait, polls until the tenant reports ready; restart_status is the wait predicate.
  • ping_aic:identity_get / identity_search / identity_create / identity_update / identity_delete — managed identities (object_type, default alpha_user), bulk through file (a JSON array or newline-delimited JSON). Outputs are masked by default: e-mail addresses, names, phone numbers and addresses reduce to a shape that identifies nothing while identifiers stay for the next step; a bulk step attempts every record and fails when any failed, with every outcome in output_file.
  • ping_aic:service_account_create / service_account_rotate / service_account_delete — the pack's own bootstrap. The key pair is generated on the runner: the public key set goes to the tenant and the private key is written to key_file (mode 0600) and nowhere else — not a step output, not the log — for a following step to store in the vault.
  • ping_aic:logs_pull — tenant log events for a source and time window (begin, end, filter, transaction_id) to a newline-delimited JSON output_file, masked by default: the evidence for an incident change.
  • ping_aic:api — the escape hatch, gated harder than the typed actions (below).

Every action pre-warms the pinned pingaic binary (PATH, then the runner cache, then the pack's own bundled tools/ directory, then a sha256-verified download — never an unpinned public URL). A pack installed from its signed bundle carries the tool, so a fresh runner needs no toolchain and no download configuration.

The gated escape hatch​

ping_aic:api dials one tenant-relative REST endpoint (method, path with {realm} substituted from realm, body, query, api_version). It refuses, with exit code 78, unless two independent gates are open:

  1. the tenant document declares api_lane: enabled — the configuration repository's owners opted the tenant in, under review;
  2. the step environment carries PING_AIC_API_LANE=enabled — an opschain.env property the estate's administrators set on the OpsChain environment, so path-based authorisation governs who may grant it and an approval policy can sit on the environment.

The pack entitlement applies on top, and the step log announces every dial through the lane. Prefer a typed action; a gap worth this lane is a gap worth reporting.

Worked examples​

Each example is a complete declarative action file (.opschain/actions/<name>.yaml) composing the pack's actions with uses: + with: (opschain docs orchestration). Nothing secret appears in any of them — credential custody is server-side — and tenant_file is omitted wherever the environment's ping_aic.tenant_file property should carry it.

Prove the resources are in sync (ping_aic:plan)​

# .opschain/actions/check_drift.yaml
action: check_drift
description: Compare the committed resources with the tenant and keep the plan as evidence
steps:
- name: plan
uses: ping_aic:plan
with: { dir: .opschain/ping/resources, plan_file: drift.txt }
- name: report
run: echo "drift=${{ steps.plan.outputs.has_changes }} (${{ steps.plan.outputs.updates }} updates)"

Reconcile, then promote with the report as evidence​

The approval that governs the promotion into production is the approval policy on the OpsChain environment this change runs under.

# .opschain/actions/release_identity_config.yaml
action: release_identity_config
description: Apply the resources, then promote the environment with a dry run first
steps:
- name: apply
uses: ping_aic:apply
with: { dir: .opschain/ping/resources, prune: "ping_aic_journey,ping_aic_script" }
- name: lock
uses: ping_aic:promotion_lock
- name: rehearse
uses: ping_aic:promotion_promote
with: { dry_run: "true", report_file: promotion-dry-run.json }
- name: promote
uses: ping_aic:promotion_promote
with: { description: "identity config release", report_file: promotion-report.json }
- name: unlock
uses: ping_aic:promotion_unlock
with: { promotion_id: '${{ steps.lock.outputs.promotion_id }}' }

Rotate a secret and apply it (ping_aic:esv_set, ping_aic:restart)​

The secret's value rides an opschain.env property carrying a vault reference; the action names the variable, never the value.

# .opschain/actions/rotate_api_key.yaml
action: rotate_api_key
description: Replace the upstream API key and restart so the tenant picks it up
steps:
- name: set
uses: ping_aic:esv_set
with: { name: esv-api-key, type: secret, value_env: PING_AIC_ESV_API_KEY, description: "upstream API key" }
- name: restart
uses: ping_aic:restart
with: { wait_timeout: 20m }

Bulk identities, masked (ping_aic:identity_create)​

# .opschain/actions/onboard_users.yaml
action: onboard_users
description: Create the quarter's new users from the committed file
steps:
- name: create
uses: ping_aic:identity_create
with: { object_type: alpha_user, file: onboarding/2026-q4.ndjson, output_file: onboarding-results.json }
- name: record
run: echo "created ${{ steps.create.outputs.succeeded }} of ${{ steps.create.outputs.count }}"

Adopt an existing configuration tree (ping_aic:adopt)​

# .opschain/actions/adopt_config.yaml
action: adopt_config
description: Convert the tenant configuration tree the team already commits into YAML resources
steps:
- name: adopt
uses: ping_aic:adopt
with: { from: config/aic-dev, to: .opschain/ping/resources }
- name: record
run: echo "adopted ${{ steps.adopt.outputs.documents }} documents; unmapped: ${{ steps.adopt.outputs.unmapped }}"

Exit codes​

64 usage · 65 validation · 68 not found · 73 conflict (a lock already held, a revision mismatch) · 75 throttled (after Retry-After-honouring retries) · 77 authorization · 78 gated · 1 anything else. Workflows branch on the class, not on message scraping.

Entitlement and lapse behaviour​

The signed-pack gate, uniformly: a LimePoint-signed pack whose verified manifest requires packs:ping-aic is gated on that feature at install and at every action run — the refusal names the pack, the mount and the missing entitlement, and lands a customer-visible licence warning event. The grant is layered by a pack-tier licence document beside your base licence and stops contributing once that document expires past its grace window: installed content and stored configuration are never removed, reads stay up, and everything lights back up when a renewed licence is applied.

Where the rest lives​

  • Entitlements — the pack vocabulary and refusal shapes.
  • The pack store — installing signed bundles.
  • Secrets providers — the vault references the service account and ESV values ride.
  • opschain docs getting-started/2-4-using-packs — mounting a pack onto a template.