cloud-foundation-fabric/fast/docs/0-domainless-iam.md

5.9 KiB

Support for domain-less organizations

authors: Ludo
reviewed by: Julio
date: Feb 12, 2024

Status

Implemented in #2064.

Context

The current FAST design assumes that operational groups come from the same Cloud Identity instance connected to the GCP organization.

While this approach has worked well in the past, there are already designs that cannot be easily mapped (for example groups coming from a separate CI), and the situation will only get worse once domain-less organizations start to be in wider use.

Removing the assumption that FAST logical principals (e.g. gcp-organization-admins) always map directly to groups is not entirely trivial, since FAST uses data from the groups variable in different places:

  • to define authoritative IAM bindings via the module-level group_iam interface
  • to define additive IAM bindings via the module-level iam_bindings_additive interface
  • to set essential contacts at the folder and project level

This proposal removes the dependency from groups by allowing to pass in to FAST any principal type, while still trying to preserve the current default behaviour and code readability in IAM bindings.

Proposal

FAST variable type change and optional interpolation

The current groups variable was meant as a simple mapping between logical profile names used internally by FAST, and actual group names. The default case was furthermore made easier by interpolating the organization domain when no domain was specified, and adding the group: principal prefix for IAM bindings.

The new proposed variable maintains the legacy behaviour, but slightly changes it so that no interpolation happens if the variable attributes have a principal prefix. The variable type is also updated to use optional, so that individual logical profile / principal mappings can be specified without having to override the whole block.

variable "groups" {
  type = object({
    gcp-billing-admins      = optional(string, "gcp-billing-admins")
    gcp-devops              = optional(string, "gcp-devops")
    gcp-network-admins      = optional(string, "gcp-network-admins")
    gcp-organization-admins = optional(string, "gcp-organization-admins")
    gcp-security-admins     = optional(string, "gcp-security-admins")
    gcp-support             = optional(string, "gcp-support")
  })
  nullable = false
  default = {}
}

Passing in different principals is intuitive:

groups = {
  gcp-devops = "principalSet://iam.googleapis.com/locations/global/workforcePools/mypool/group/abc123"
  gcp-organization-admins = "group:gcp-organization-admins@other.domain"
}

Internally, interpolation is fairly straightforward:

locals {
  groups = {
    for k, v in var.group_principals : k => (
      can(regex("^[a-zA-Z]+:", v))
      ? v
      : "group:${v}@${var.organization.domain}"
    )
  }
}

FAST IAM additive bindings and module interface change

FAST leverages the group_iam module-level interface to improve code readability for authoritative bindings, which is a primary goal of the framework. Introducing support for any principal type prevents us from using this interface, with a non-trivial impact on the overall readability of IAM roles in FAST.

This is an example use in the IaC project:

  # human (groups) IAM bindings
  group_iam = {
    (local.groups.gcp-devops) = [
      "roles/iam.serviceAccountAdmin",
      "roles/iam.serviceAccountTokenCreator",
    ]
    (local.groups.gcp-organization-admins) = [
      "roles/iam.serviceAccountTokenCreator",
      "roles/iam.workloadIdentityPoolAdmin"
    ]
  }

This proposal addresses the issue by changing the module-level interface to support different principal types. The original goal for group_iam -- to allow for better readability -- is preserved at the cost of the slight increase in verbosity due to having to specify the principal type.

The trade-off in verbosity seems acceptable as it makes the new interface more flexible, and allows using the interface for principal: and principalSet: types, which are becoming more and more important to support.

FAST code remains unchanged, as the groups local already contains a prefix for each principal, either interpolated or passed in by the user.

The module-level variable definition changes only its name and description:

variable "iam_by_principals" {
  description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
  type        = map(list(string))
  default     = {}
  nullable    = false
}

Actual use is basically unchanged from the current group_iam interface:

# current interface
  group_iam = {
    "app1-admins@example.org" = [
      "roles/owner",
      "roles/resourcemanager.folderAdmin",
      "roles/resourcemanager.projectCreator"
    ]
  }
# proposed interface
  iam_by_principals = {
    "group:app1-admins@example.org" = [
      "roles/owner",
      "roles/resourcemanager.folderAdmin",
      "roles/resourcemanager.projectCreator"
    ]
    "principalSet://iam.googleapis.com/locations/global/workforcePools/mypool/group/abc123": = [
      "roles/owner",
      "roles/resourcemanager.folderAdmin",
      "roles/resourcemanager.projectCreator"
    ]
  }

FAST essential contacts

Having group_principals support different type of principals will make it impossible to use the same variable to set essential contacts, as the principal might not be a group.

This will require introduction of a new essential_contacts top-level variable keyed by folder/project (the individual contexts on which to set contacts), with the added benefit of being able to specify different and potentially multiple contacts compared to now.

Decision

Rolled out.