# **Technical Design Document: Gemini Enterprise License Management Utility**

## **1. Overview**

This document outlines the technical design for a standalone utility that provides robust, policy-driven license management for Gemini Enterprise. The solution addresses the need for granular, group-based license assignment, which is a critical requirement for large enterprise customers. It is designed to be deployed and managed by the customer within the customer's own Google Cloud project.

The core of the utility is a **Cloud Run Job** that executes scheduled reconciliation and garbage-collection tasks based on a provided JSON configuration.

## **2. Core Application Design & Hosting**

### **2.1. Artifact**

The utility will be delivered as a **Cloud Run Job** container image. This encapsulates all application logic, ensuring consistency and simplifying deployment.

### **2.2. Hosting**

The utility is designed for deployment on **Cloud Run Jobs**. This provides a fully managed, serverless environment that runs to completion and exits — there is no persistent HTTP server. The job scales to zero between executions, minimizing operational overhead and cost.

### **2.3. Performance & Execution**

The utility operates on an eventually consistent model — there is no requirement for "always-on" operation or sub-second response times. Jobs are triggered by Cloud Scheduler on a periodic basis and run to completion.

Cloud Run Jobs support a task timeout of up to **168 hours**, which accommodates organizations with very large user populations. Native task-level parallelism is available via `CLOUD_RUN_TASK_INDEX` and `CLOUD_RUN_TASK_COUNT` environment variables (injected automatically by Cloud Run), allowing the configured project list to be sharded across multiple concurrent task instances.

## **3. Data Storage & Configuration**

The utility uses a JSON-based configuration model which is persisted to GCP Secret Manager.

### **3.1. Entitlement Configuration (Secret Manager)**

Configuration is supplied by administrators as a JSON object stored in **Google Cloud Secret Manager**. This secret is mapped as a **volume mount** to the Cloud Run Job, allowing the utility to read the configuration from a local file path (e.g., `/run/secrets/entitlements.json`) at runtime. This approach ensures that group mappings are managed securely and can be updated independently of the function code.

**Example Configuration Structure:**

```json
{
  "billing_account_id": "ABCDE-12345-FGHIJ",
  "projects": {
    "customer-project-alpha": [
      {
        "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE",
        "location": "global",
        "groups": [
          "group-data-scientists@example.com",
          "group-senior-devs@example.com"
        ]
      },
      {
        "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS",
        "location": "global",
        "groups": [
          "group-marketing@example.com",
          "group-sales@example.com",
          "group-general-staff@example.com"
        ]
      }
    ]
  },
  "settings": {
    "staleness_threshold_days": 30
  }
}
```

**Top-level fields:**

| Field | Type | Required | Description |
| ----- | ----- | ----- | ----- |
| `billing_account_id` | string | Yes | The GCP billing account ID associated with the managed projects. |
| `projects` | object | Yes | Map of GCP project ID → list of entitlement entries. |
| `settings.staleness_threshold_days` | integer | No | Users inactive beyond this many days are revoked. `0` (default) disables the staleness check. |

**Per-project entry fields:**

Each project maps to an ordered array of entitlement entries. A project may have multiple entries — one per SKU/location combination.

| Field | Type | Required | Description |
| ----- | ----- | ----- | ----- |
| `subscription_tier` | string | Yes | The Gemini SKU to assign. Must be a valid `SubscriptionTier` enum value (see section 5.3). |
| `location` | string | Yes | Geographic region for license management. Must be one of: `global`, `us`, `eu`. |
| `groups` | array of strings | Yes | List of Google Group email addresses whose members are entitled to this SKU. |
```

## **4. Job Definitions & Triggers**

The utility is packaged as a single container image and deployed as **two Cloud Run Job definitions**, each configured with a different `JOB_TYPE` environment variable. Both jobs are triggered by Cloud Scheduler via the Cloud Run Jobs API on independent schedules.

| Job Definition | `JOB_TYPE` | Schedule | Description |
| ----- | ----- | ----- | ----- |
| `gemini-box-office-joiner` | `joiner` | Daily (24hr) | Finds and grants licenses to members of entitled groups. |
| `gemini-box-office-gc` | `gc` | Every 6 hours | Finds and revokes licenses from stale or unentitled users. |

### **4.1. Environment Variables**

| Variable | Source | Description | Default |
| ----- | ----- | ----- | ----- |
| `JOB_TYPE` | Job definition | Selects the workflow to execute (`joiner` or `gc`). Required. | — |
| `DRY_RUN` | Job definition / per-execution override | When `true`, evaluation runs in full but no write API calls are made. Can be overridden per execution via the Cloud Scheduler request body. | `false` |
| `CLOUD_RUN_TASK_INDEX` | Injected by Cloud Run | 0-based index of this task instance. Used to shard the project list. | `0` |
| `CLOUD_RUN_TASK_COUNT` | Injected by Cloud Run | Total number of concurrent task instances. | `1` |

### **4.2. Project-Level Sharding**

When `CLOUD_RUN_TASK_COUNT` is greater than `1`, each task instance processes only the projects assigned to it: projects where `projectIndex % CLOUD_RUN_TASK_COUNT == CLOUD_RUN_TASK_INDEX`. This allows all configured projects to be processed in parallel with no shared state between tasks.

## **5. Detailed Workflows & External API Integrations**

The utility's logic is divided into two primary reconciliation workflows.

### **5.1. Workflow 1: Bulk "Joiner" Job (Scheduled: 24hr)**

* **Trigger:** Cloud Scheduler via the Cloud Run Jobs API (`JOB_TYPE=joiner`).
* **External APIs:**
  1. **Cloud Identity Admin API:** [`members.list`](https://developers.google.com/workspace/admin/directory/reference/rest/v1/members/list) — Go client: [`google.golang.org/api/admin/directory/v1`](https://pkg.go.dev/google.golang.org/api/admin/directory/v1)
  2. **Discovery Engine API:** [`batchUpdateUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1), [`licenseConfigsUsageStats.list`](https://cloud.google.com/go/discoveryengine/apiv1)
* **Flow:**
  1. For each group in the configuration, it calls the **Cloud Identity Admin API's** `members.list` method (with `includeDerivedMembership=true`).
  2. The job handles cursor-based pagination to traverse all members, including those in nested groups.
  3. For each member that is a `User`, it calls the **Discovery Engine API** (`batchUpdateUserLicenses`) to assign the license. This operation is idempotent.

* **License Pool Exhaustion Handling:**
  A billing account may have multiple active subscriptions for the same SKU. At startup, the joiner fetches all `BillingAccountLicenseConfig` records and builds an index keyed by `(SKU, ProjectNumber, Location)`. Each key maps to an ordered slice of `LicenseConfigEntry` values — one per distinct subscription pool — so that all seat capacity is visible and no pool is silently discarded.

  If a `batchUpdateUserLicenses` call fails because a pool is exhausted, the job does **not** treat this as a fatal error. Instead it:
  1. Calls `licenseConfigsUsageStats.list` for the affected project to retrieve the current `usedLicenseCount` for the relevant `licenseConfig`.
  2. Computes `available = allocatedCount - usedLicenseCount` (the allocated count is from the `licenseConfigDistributions` map fetched at startup).
  3. Retries the batch trimmed to `available` items, granting as many licenses as possible.
  4. Carries the remaining (ungranted) users forward to the next subscription pool for the same SKU, if one exists, and repeats steps 1–3.
  5. After all pools for a SKU are exhausted, soft-fails any users still unseated: logs a `WARN` entry per exhausted pool and continues to the next batch or project. The job exits `0`.

  Each user appears in at most one successful `batchUpdateUserLicenses` call — users do not advance to the next pool if they were already granted a seat. The `licenses_soft_failed` count in the final summary reflects only users who could not be seated across all pools for their SKU.

### **5.2. Workflow 2: Bulk "Garbage Collection" Job (Scheduled: 6hr)**

* **Trigger:** Cloud Scheduler via the Cloud Run Jobs API (`JOB_TYPE=gc`).
* **External APIs:**  
  1. **Discovery Engine API:** [`listUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1) and [`batchUpdateUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1).  
  2. **Cloud Identity Admin API:** [`members.hasMember`](https://developers.google.com/workspace/admin/directory/reference/rest/v1/members/hasMember) — Go client: [`google.golang.org/api/admin/directory/v1`](https://pkg.go.dev/google.golang.org/api/admin/directory/v1)
* **Flow:**  
  1. Iterates through all licensed users via the **Discovery Engine API** (`listUserLicenses`).  
  2. For each user, the job evaluates two deprovisioning conditions:
      - **Staleness**: If `staleness_threshold_days` is greater than `0`, checks whether the user's staleness reference date is more than `X` days ago. The reference date is chosen as follows: if `lastLoginTime` is set, it is used; if `lastLoginTime` is absent (the user has never logged in), `createTime` (the license assignment timestamp) is used instead, so that a recently provisioned account is not immediately revoked before the user has had a chance to sign in. If both timestamps are absent, the user is treated as immediately stale. Both timestamps are retrieved from the `UserLicense` object provided by the **Discovery Engine API**. Setting `staleness_threshold_days` to `0` (or omitting it) disables this check entirely — only entitlement is evaluated.
      - **Entitlement**: Calls the **Cloud Identity Admin API's** `members.hasMember` method to confirm if the user is still a member of any entitled group. If the API returns HTTP 400 (invalid member key — e.g. a typo'd email entered manually via the Discovery Engine UI), the user is skipped with a structured `WARN` log entry and the job continues; no revocation is performed for that user.
  3. If **either** condition is met (the user is stale OR no longer entitled), the utility calls the **Discovery Engine API** (`batchUpdateUserLicenses`) to revoke the license.

### **5.3. License Precedence Logic**

In scenarios where a user belongs to multiple groups mapped to different SKUs, the utility applies a hardcoded precedence ranking to ensure the highest entitlement is granted.

**Precedence Ranking (Highest to Lowest):**
1. `SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT`
2. `SUBSCRIPTION_TIER_ENTERPRISE`
3. `SUBSCRIPTION_TIER_SEARCH`
4. `SUBSCRIPTION_TIER_NOTEBOOK_LM`
5. `SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS`
6. `SUBSCRIPTION_TIER_AGENTSPACE_STARTER`
7. `SUBSCRIPTION_TIER_FRONTLINE_WORKER`
8. `SUBSCRIPTION_TIER_FRONTLINE_STARTER`
9. `SUBSCRIPTION_TIER_ENTERPRISE_EMERGING`
10. `SUBSCRIPTION_TIER_EDU_PRO`
11. `SUBSCRIPTION_TIER_EDU`
12. `SUBSCRIPTION_TIER_EDU_PRO_EMERGING`
13. `SUBSCRIPTION_TIER_EDU_EMERGING`

If a user is eligible for multiple SKUs, the "Joiner" job will only attempt to assign the highest-ranked SKU. The "Garbage Collection" job will not revoke a higher-tier license if the user is still entitled to it, even if they are removed from a lower-tier group.

## **6. Authentication & Authorization** {#6.-authentication-&-authorization}

Two distinct layers of security must be implemented:

1. **Service-Level Authorization:** The utility's Cloud Run Job must run with a dedicated **Google Cloud Service Account**. This account requires the following IAM roles and OAuth scopes:

    **IAM Roles:**
    *   `roles/discoveryengine.admin`: Required to list and update user licenses.
    *   `roles/cloudidentity.groups.viewer`: Required to list group members and verify membership.
    *   `roles/secretmanager.secretAccessor`: Required to read the entitlement configuration mounted from Secret Manager.

    **OAuth Scopes:**
    *   `https://www.googleapis.com/auth/cloud-platform`
    *   `https://www.googleapis.com/auth/cloud-identity.groups.readonly`

    *Note: Domain-Wide Delegation is not required as the Cloud Identity API honors IAM-based permissions at the Organization/Folder level.*

2. **Trigger Authorization:** Access to the Cloud Run Jobs must be restricted to authorized callers. The Cloud Scheduler service account must be granted `roles/run.invoker` on each job resource. No HTTP endpoint is exposed.

## **7. Non-Functional Requirements** {#7.-non-functional-requirements}

* **API Rate Limiting & Concurrency:**
    *   **Request Batching:** The utility MUST utilize the `batchUpdateUserLicenses` endpoint to consolidate up to 100 license modifications (assignments or revocations) per API request, minimizing round-trip latency and quota consumption.
    *   **Backoff:** Exponential backoff will be applied to all 429 and 5xx errors using a standard retry library.
    *   **License Pool Exhaustion:** License pool exhaustion (`ErrLicensesExhausted`) is treated as a soft failure distinct from rate limiting (`ErrAPIRateLimited`). Exhaustion triggers a `licenseConfigsUsageStats.list` lookup, a trimmed retry for remaining available seats, and a structured warning log — it does not abort the job.
    *   **Invalid Member Key (GC only):** If `members.hasMember` returns HTTP 400 (`ErrInvalidMemberKey`) for a licensed user — indicating the stored email is malformed or otherwise unresolvable by the Admin API — the user is skipped with a `WARN` log entry. No revocation is issued and the job continues. This guards against typo'd emails entered via the Discovery Engine UI, which lacks input validation.
* **Structured Logging:**
    *   **Format:** All logs MUST be emitted in JSON format to stdout for automatic ingestion by Cloud Logging.
    *   **Library:** Use Go's standard `log/slog` library.
    *   **Fields:** Every log entry MUST include `severity`, `workflow` (`joiner` | `garbage_collection`), and `message`.
    *   **Contextual Fields:** Include `group_id`, `sku_id`, and `duration_ms` where applicable.
    *   **PII Protection:** `user_email` and other personally identifiable information MUST NOT be logged. `group_id` is the primary identifier for reconciliation context.
* **Multi-Project Deployment:** A single job definition handles all configured projects. For customers with very large project portfolios, `CLOUD_RUN_TASK_COUNT` can be increased to shard the project list across parallel task instances (see section 4.2).
* **Automation:** All components should be deployable with example gCloud commands provided in this repository.
