Skip to content

[Bug] gemini-enterprise time-based access: the configured time zone is not honored — a re-run silently resets it to America/New_York, and the level's title and hour descriptions assert Eastern regardless of what was set #187

Description

@JohnHales

Bug Description

Three defects in the same feature, all of which make a non-Eastern deployment look and behave as Eastern. They share ten lines of code, so they are reported together; splitting them is fine if preferred.

1. The hour variables are documented as Eastern, unconditionally

# gemini-stage-0/variables.tf:102-110
variable "access_start_hour" {
  description = "The hour (0-23) in America/New_York timezone when access starts."
}
variable "access_end_hour" {
  description = "The hour (0-23) in America/New_York timezone when access ends."
}

Both descriptions name America/New_York as the timezone the hour is expressed in. That is only true when access_time_zone is left at its default. The expression actually evaluates the hour in whatever access_time_zone is set to (access_policy.tf:51request.time.getHours("${var.access_time_zone}")), so for any other timezone the documentation is simply wrong about what the number means. An operator configuring America/Anchorage reads that access_start_hour = 8 is 8am Eastern; it is 8am Alaska.

2. The access level's title hardcodes Eastern

# gemini-stage-0/access_policy.tf:48
title = "Business Hours East Coast"

This never varies with access_time_zone. Anyone reading the level in the Console or via gcloud access-context-manager levels describe time sees "Business Hours East Coast" asserted over a window that may be Alaska, Pacific or anything else. Combined with defect 1, every user-facing surface of this feature claims Eastern while the evaluation is doing something different — which is precisely the state that makes a misconfiguration invisible.

To be clear about what is NOT broken: the CEL expression itself is correct. It substitutes var.access_time_zone into all four getHours / getDayOfWeek calls, so a level built from a fresh, complete run evaluates in the configured zone. The bug is that the labels and docs say otherwise — and defect 3 then throws the configured value away entirely.

3. "Preserving" silently overwrites the configured window with the defaults

When the time access level already exists and is Terraform-managed, deploy.sh reports that it is preserving it and skips every time-related prompt:

# deploy.sh:1143-1150
if echo "$EXISTING_LEVELS" | grep -qE "(/|^)time$"; then
    if [[ "$MANAGED_ACCESS_LEVELS" == *"time"* ]]; then
         echo -e "${GREEN}Found existing MANAGED Access Level 'time'. Preserving.${NC}"
         CREATE_TIME_ACCESS="true"
    else
         echo -e "${YELLOW}Access Level 'time' already exists (Unmanaged). Skipping.${NC}"
         CREATE_TIME_ACCESS="false"
    fi
else
    # the five prompts — start day, end day, start hour, end hour, TIME ZONE — live here
fi

Because the prompts are skipped, ACCESS_START_DAY, ACCESS_END_DAY, ACCESS_START_HOUR, ACCESS_END_HOUR and ACCESS_TIME_ZONE are all unset. They are never hydrated from prior state either — the only assignments to any of them in the whole script are the prompts at :1155-1164.

The tfvars writer then skips each one, because every write is guarded on the variable being non-empty:

# deploy.sh:2455-2469
if [[ -n "$ACCESS_START_DAY" ]];  then echo "access_start_day = ${ACCESS_START_DAY}"   >> gemini-stage-0/terraform.tfvars; fi
...
if [[ -n "$ACCESS_TIME_ZONE" ]];  then echo "access_time_zone = \"${ACCESS_TIME_ZONE}\"" >> gemini-stage-0/terraform.tfvars; fi

So nothing is written, and Terraform falls back to the variable defaults — access_time_zone = "America/New_York", access_start_hour = 7, access_end_hour = 21, access_start_day = 1, access_end_day = 5.

Meanwhile CREATE_TIME_ACCESS="true" means the resource is still fully managed, and its expression is rebuilt from exactly those variables:

# gemini-stage-0/access_policy.tf:44-53
resource "google_access_context_manager_access_level" "time" {
  count = var.access_policy_number != "" && var.create_time_access ? 1 : 0
  title = "Business Hours East Coast"
  custom { expr {
    expression = ("request.time.getHours(\"${var.access_time_zone}\") >= ${var.access_start_hour} && ... <= ${var.access_end_day}")
  }}
}

The apply therefore overwrites the operator's configured window with the East-Coast defaults, immediately after printing "Preserving." The script reports the opposite of what it does.

The expire level has the identical defect (deploy.sh:1173-1180): "Preserving" + CREATE_EXPIRE_ACCESS="true", no prompt, ACCESS_EXPIRATION_TIMESTAMP unset, not written, so the expiry silently resets to the default 2028-01-01T00:00:00Z. The us level shares the shape but carries no parameters, so nothing is lost there.

Environment and Deployment Context

  • Stellar Engine Version/Commit: main at commit f64ce6cd (= tag v3.0.0), verified 2026-08-11
  • Deployment Type:
    • US Region Restricted (e.g., Access Policy constraint)
    • FedRAMP Medium
    • FedRAMP High
    • FedRAMP Moderate
    • DoD IL4
    • DoD IL5
    • Stand-alone / Custom
  • FAST Stage (if applicable): N/A — this is a blueprint, not a FAST stage
    • Stage 0 (Bootstrap)
    • Stage 1 (Resource Management)
    • Stage 2 (Network Creation)
    • Stage 3 (Security and Audit)
  • Affected Component:
    • blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/variables.tf:102-110 (hour descriptions hardcoding America/New_York)
    • blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/access_policy.tf:48 (title hardcoding Business Hours East Coast)
    • blueprints/fedramp-high/gemini-enterprise/deploy.sh:1143-1150 (the "Preserving" branch that skips the prompts)
    • blueprints/fedramp-high/gemini-enterprise/deploy.sh:1155-1164 (the only assignments to the five variables)
    • blueprints/fedramp-high/gemini-enterprise/deploy.sh:2455-2469 (writes guarded on non-empty)
    • blueprints/fedramp-high/gemini-enterprise/deploy.sh:1173-1180 (same defect for expire)
    • blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/access_policy.tf:44-53 (the managed resource)
  • Terraform Version: 1.12.2 (pinned by deploy.sh via tfenv; the stage declares required_version >= 1.7.4)
  • GCP Provider Version: hashicorp/google >= 6.21.0 (stage-0 declared constraint)

Steps to Reproduce

  1. Run deploy.sh, answer y to time-based access, and set a non-default window — e.g. Time Zone America/Anchorage, start hour 8, end hour 18.
  2. Complete the apply. Read the level back and confirm the expression carries America/Anchorage and >= 8 && <= 18.
  3. Run deploy.sh again against the same environment — any second pass, which is the normal path for adding an application or re-running a stage.
  4. Observe Found existing MANAGED Access Level 'time'. Preserving. and note that no time prompts appear.
  5. Complete the apply and read the level again: gcloud access-context-manager levels describe time --policy <access_policy_number>.

Expected Behavior

The configured time zone is honored and represented consistently. Specifically: the hour descriptions say the hour is expressed in access_time_zone, not in America/New_York; the level's title reflects the configured zone rather than asserting Eastern; and "Preserving" preserves — either the existing values are read back and re-written unchanged, or the operator is re-prompted with them as defaults, or the resource is left unmanaged so Terraform does not touch it.

Actual Behavior

Every user-facing surface asserts Eastern regardless of configuration, and the configured value does not survive a second run:

  • access_start_hour / access_end_hour are documented as being "in America/New_York timezone" whatever access_time_zone is set to.
  • The level is titled Business Hours East Coast whatever access_time_zone is set to.
  • On any re-run through the wizard, the level is rewritten to the defaults: timezone reverts to America/New_York, hours to 07:00-21:00, days to Mon-Fri. For a deployment configured in America/Anchorage that shifts the effective window by four hours — an 08:00-18:00 Alaska window silently becomes 07:00-21:00 Eastern, i.e. 03:00-17:00 local. No warning, no prompt, and the console line says the opposite.

The net effect is that an operator who sets a non-Eastern zone has no surface anywhere — variable docs, level title, or a second wizard run — that agrees with what they configured.

Relevant Logs and Errors

No error is emitted — that is the defect. The only operator-visible signal is the line that misdescribes what happens:

--- Time Based Access ---
Found existing MANAGED Access Level 'time'. Preserving.

The resulting expression, rebuilt from the defaults:

request.time.getHours("America/New_York") >= 7 && request.time.getHours("America/New_York") <= 21 &&
request.time.getDayOfWeek("America/New_York") >= 1 && request.time.getDayOfWeek("America/New_York") <= 5

Suggested Fix

Defects 1 and 2 are one-liners. Change the access_start_hour / access_end_hour descriptions to say the hour is expressed in access_time_zone (naming America/New_York only as the default), and make the level title carry the configured zone — e.g. "Business Hours (${var.access_time_zone})" — instead of hardcoding East Coast.

Defect 3, in the "Preserving" branch, do one of:

  1. Read the existing level back and populate ACCESS_* from it before the tfvars write, so the values round-trip; or
  2. Re-prompt using the existing values as the defaults; or
  3. Set CREATE_TIME_ACCESS="false" so Terraform stops managing a level the script has decided not to reconfigure — matching what "Preserving" already implies.

Whichever is chosen, the console message must match the behavior, and the same fix applies to the expire branch.

Additional Context

Found while running a deployment configured for America/Anchorage. Defect 3 is security-relevant rather than cosmetic: a time-based access control silently shifts its effective window on an ordinary re-run, and an operator who reads the "Preserving" line has no reason to re-check it. Defects 1 and 2 are what make it hard to catch — every label and description the operator can consult says Eastern, so the reset back to Eastern looks like the intended state.

Related: #186 covers the day-numbering mismatch in the same expression — the day prompts at deploy.sh:1155 / :1157 and the access_start_day / access_end_day descriptions all state 1=Mon, 7=Sun while CEL's getDayOfWeek() is 0-6. Between the two reports, every input to this one access level is either mis-documented or discarded on re-run; they are worth fixing together.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions