diff --git a/infra/gcp/ensure-organization.sh b/infra/gcp/ensure-organization.sh index 0b31d717d15..64b3f3d5aed 100755 --- a/infra/gcp/ensure-organization.sh +++ b/infra/gcp/ensure-organization.sh @@ -40,7 +40,7 @@ fi ## setup custom role for prow troubleshooting color 6 "Ensuring custom org role prow.viewer role exists" ( - ensure_custom_org_role_from_file "prow.viewer" "${SCRIPT_DIR}/roles/prow.viewer.yaml" + ensure_custom_iam_role_from_file "org" "prow.viewer" "${SCRIPT_DIR}/roles/prow.viewer.yaml" ) 2>&1 | indent color 6 "Ensuring org-level IAM bindings exist" diff --git a/infra/gcp/lib.sh b/infra/gcp/lib.sh index 82f6501decf..fd5c200f0d3 100755 --- a/infra/gcp/lib.sh +++ b/infra/gcp/lib.sh @@ -17,6 +17,7 @@ # This is a library of functions used to create GCP stuff. . "$(dirname "${BASH_SOURCE[0]}")/lib_util.sh" +. "$(dirname "${BASH_SOURCE[0]}")/lib_iam.sh" . "$(dirname "${BASH_SOURCE[0]}")/lib_gcr.sh" . "$(dirname "${BASH_SOURCE[0]}")/lib_gcs.sh" @@ -576,100 +577,3 @@ function ensure_regional_address() { --region="${region}" fi } - -# Ensure that custom IAM role exists, creating one if needed -# Arguments: -# $1: The GCP project -# $2: The role name (e.g. "ServiceAccountLister") -# $3: The role title (e.g. "Service Account Lister") -# $4: The role description (e.g. "Can list ServiceAccounts.") -# $5+: The role permissions (e.g. "iam.serviceAccounts.list") -# Example usage: -# ensure_custom_iam_role \ -# kubernetes-public \ -# ServiceAccountLister \ -# "Service Account Lister" \ -# "Can list ServiceAccounts." \ -# iam.serviceAccounts.list -function ensure_custom_iam_role() { - if [ $# -lt 5 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] \ - || [ -z "${4}" ] || [ -z "${5}" ] - then - echo -n "ensure_custom_iam_role(gcp_project, name, title," >&2 - echo " description, permission...) requires at least 5 arguments" >&2 - return 1 - fi - - local gcp_project="${1}"; shift - local name="${1}"; shift - local title="${1}"; shift - local description="${1}"; shift - local permissions; permissions=$(join_by , "$@") - - if ! gcloud --project "${gcp_project}" iam roles describe "${name}" \ - >/dev/null 2>&1 - then - gcloud --project "${gcp_project}" --quiet \ - iam roles create "${name}" \ - --title "${title}" \ - --description "${description}" \ - --stage GA \ - --permissions "${permissions}" - fi -} - -# Ensure that custom IAM role exists and is in sync with definition in file -# Arguments: -# $1: The role name (e.g. "prow.viewer") -# $2: The file (e.g. "/path/to/file.yaml") -function ensure_custom_org_role_from_file() { - if [ ! $# -eq 2 -o -z "$1" -o -z "$2" ]; then - echo "ensure_custom_org_role_from_file(name, file) requires 2 arguments" >&2 - return 1 - fi - - local org="${GCP_ORG}" - local name="${1}" - local file="${2}" - - if ! gcloud iam roles describe "${name}" --organization "${org}" \ - >/dev/null 2>&1 - then - # be noisy when creating a role - gcloud iam roles create "${name}" --organization "${org}" --file "${file}" - else - # be quiet when updating, only output name of role - gcloud iam roles update "${name}" --organization "${org}" --file "${file}" | grep ^name: - fi -} - -function custom_org_role_name() { - if [ ! $# -eq 1 -o -z "$1" ]; then - echo "custom_org_role_name(name) requires 1 arguments" >&2 - return 1 - fi - - local name="${1}" - - echo "organizations/${GCP_ORG}/roles/${name}" -} - -# Ensure that IAM binding exists at org level -# Arguments: -# $1: The role name (e.g. "prow.viewer") -# $2: The file (e.g. "/path/to/file.yaml") -function ensure_org_role_binding() { - if [ ! $# -eq 2 -o -z "$1" -o -z "$2" ]; then - echo "ensure_org_role_binding(principal, role) requires 2 arguments" >&2 - return 1 - fi - - local org="${GCP_ORG}" - local principal="${1}" - local role="${2}" - - gcloud \ - organizations add-iam-policy-binding "${GCP_ORG}" \ - --member "${principal}" \ - --role "${role}" -} diff --git a/infra/gcp/lib_iam.sh b/infra/gcp/lib_iam.sh new file mode 100644 index 00000000000..bf9c1ff5566 --- /dev/null +++ b/infra/gcp/lib_iam.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# +# Copyright 2021 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# IAM utility functions +# +# This is intended to be very general-purpose and "low-level". Higher-level +# policy does not belong here. +# +# This MUST NOT be used directly. Source it via lib.sh instead. + +# Ensure that custom IAM role exists, creating one if needed +# Arguments: +# $1: The GCP project +# $2: The role name (e.g. "ServiceAccountLister") +# $3: The role title (e.g. "Service Account Lister") +# $4: The role description (e.g. "Can list ServiceAccounts.") +# $5+: The role permissions (e.g. "iam.serviceAccounts.list") +# Example usage: +# ensure_custom_iam_role \ +# kubernetes-public \ +# ServiceAccountLister \ +# "Service Account Lister" \ +# "Can list ServiceAccounts." \ +# iam.serviceAccounts.list +function ensure_custom_iam_role() { + if [ $# -lt 5 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] \ + || [ -z "${4}" ] || [ -z "${5}" ] + then + echo -n "ensure_custom_iam_role(gcp_project, name, title," >&2 + echo " description, permission...) requires at least 5 arguments" >&2 + return 1 + fi + + local gcp_project="${1}"; shift + local name="${1}"; shift + local title="${1}"; shift + local description="${1}"; shift + local permissions; permissions=$(join_by , "$@") + + if ! gcloud --project "${gcp_project}" iam roles describe "${name}" \ + >/dev/null 2>&1 + then + gcloud --project "${gcp_project}" --quiet \ + iam roles create "${name}" \ + --title "${title}" \ + --description "${description}" \ + --stage GA \ + --permissions "${permissions}" + fi +} + +# Ensure that custom IAM role exists and is in sync with definition in file +# Arguments: +# $1: The scope of the role (e.g. "org", "project:foobar") +# $2: The role name (e.g. "prow.viewer") +# $3: The file (e.g. "/path/to/file.yaml") +function ensure_custom_iam_role_from_file() { + if [ ! $# -eq 3 -o -z "$1" -o -z "$2" -o -z "$3" ]; then + echo "ensure_custom_iam_role_from_file(scope, name, file) requires 3 arguments" >&2 + return 1 + fi + + local scope="${1}" + local name="${2}" + local file="${3}" + + scope_flag="" + if [[ "${scope}" == "org" ]]; then + scope_flag="--organization ${GCP_ORG}" + elif [[ "${scope}" =~ "^project:" ]]; then + scope_flag="--project $(echo ${scope} | cut -d: -f2-)" + else + echo "ensure_custom_iam_role_from_file(scope, name, file) scope must be one of 'org' or 'project:project-id'" >&2 + return 1 + fi + + if ! gcloud iam roles describe ${scope_flag} "${name}" \ + >/dev/null 2>&1 + then + # be noisy when creating a role + gcloud iam roles create ${scope_flag} "${name}" --file "${file}" + else + # be quiet when updating, only output name of role + gcloud iam roles update ${scope_flag} "${name}" --file "${file}" | grep ^name: + fi +} + +# Return the full name of a custom IAM role defined at the org level +# Arguments: +# $1: The role name (e.g. "prow.viewer") +function custom_org_role_name() { + if [ ! $# -eq 1 -o -z "$1" ]; then + echo "custom_org_role_name(name) requires 1 arguments" >&2 + return 1 + fi + + local name="${1}" + + echo "organizations/${GCP_ORG}/roles/${name}" +} + +# Ensure that IAM binding exists at org level +# Arguments: +# $1: The role name (e.g. "prow.viewer") +# $2: The file (e.g. "/path/to/file.yaml") +function ensure_org_role_binding() { + if [ ! $# -eq 2 -o -z "$1" -o -z "$2" ]; then + echo "ensure_org_role_binding(principal, role) requires 2 arguments" >&2 + return 1 + fi + + local org="${GCP_ORG}" + local principal="${1}" + local role="${2}" + + gcloud \ + organizations add-iam-policy-binding "${GCP_ORG}" \ + --member "${principal}" \ + --role "${role}" +} + +# Ensure that IAM binding exists at project level +# Arguments: +# $1: The project id (e.g. "k8s-infra-foo") +# $2: The principal (e.g. "group:k8s-infra-foo@kubernetes.io") +# $3: The role name (e.g. "roles/storage.objectAdmin") +function ensure_project_role_binding() { + if [ ! $# -eq 3 -o -z "$1" -o -z "$2" -o -z "$3" ]; then + echo "ensure_project_role_binding(project, principal, role) requires 3 arguments" >&2 + return 1 + fi + + local project="${1}" + local principal="${2}" + local role="${3}" + + gcloud \ + projects add-iam-policy-binding "${project}" \ + --member "${principal}" \ + --role "${role}" +} + +# Ensure that IAM binding has been removed at project level +# Arguments: +# $1: The project id (e.g. "k8s-infra-foo") +# $2: The principal (e.g. "group:k8s-infra-foo@kubernetes.io") +# $3: The role name (e.g. "roles/foo.bar") +function ensure_removed_project_role_binding() { + if [ ! $# -eq 3 -o -z "$1" -o -z "$2" -o -z "$3" ]; then + echo "ensure_removed_project_role_binding(project, principal, role) requires 3 arguments" >&2 + return 1 + fi + local project="${1}" + local principal="${2}" + local role="${3}" + + _ensure_removed_resource_role_binding "projects" "${project}" "${principal}" "${role}" +} + +# Ensure that IAM binding has been removed at resource level +# Arguments: +# $1: The resource type (e.g. "projects", "organizations", "secrets" ) +# $2: The id of the resource (e.g. "k8s-infra-foo", "12345") +# $3: The principal (e.g. "group:k8s-infra-foo@kubernetes.io") +# $4: The role name (e.g. "roles/foo.bar") +function _ensure_removed_resource_role_binding() { + if [ ! $# -eq 4 -o -z "$1" -o -z "$2" -o -z "$3" -o -z "$4" ]; then + echo "ensure_removed_project_role_binding(resource, id, principal, role) requires 4 arguments" >&2 + return 1 + fi + + local resource="${1}" + local id="${2}" + local principal="${3}" + local role="${4}" + + # gcloud remove-iam-policy-binding errors if binding doesn't exist, so confirm it does + if gcloud "${resource}" get-iam-policy "${id}" \ + --flatten="bindings[].members" \ + --format='value(bindings.role)' \ + --filter="bindings.members='${principal}' AND bindings.role='${role}'" | grep -q "${role}"; then + gcloud \ + "${resource}" remove-iam-policy-binding "${id}" \ + --member "${principal}" \ + --role "${role}" + fi +} diff --git a/infra/gcp/prow/ensure-e2e-projects.sh b/infra/gcp/prow/ensure-e2e-projects.sh index e566a7560df..32b2c596914 100755 --- a/infra/gcp/prow/ensure-e2e-projects.sh +++ b/infra/gcp/prow/ensure-e2e-projects.sh @@ -22,7 +22,7 @@ set -o nounset set -o pipefail SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") -. "${SCRIPT_DIR}/lib.sh" +. "${SCRIPT_DIR}/../lib.sh" function usage() { echo "usage: $0 [repo...]" > /dev/stderr @@ -111,6 +111,16 @@ for prj; do ( ensure_project "${prj}" + color 6 "Ensure stale role bindings have been removed from e2e project: ${prj}" + ( + # TODO(https://github.com/kubernetes/k8s.io/issues/1661): remove once verified as consistent + color 6 "group:k8s-infra-prow-viewers@kubernetes.io should not have roles/viewer (ref: https://github.com/kubernetes/k8s.io/issues/1661)" + ensure_removed_project_role_binding "${prj}" "group:k8s-infra-prow-viewers@kubernetes.io" "roles/viewer" + # TODO(https://github.com/kubernetes/k8s.io/issues/299): remove once smarter logic folded into ensure_project + color 6 "user:* should not have roles/owner for projects (ref: https://github.com/kubernetes/k8s.io/issues/299)" + ensure_removed_project_role_binding "${prj}" "spiffxp@google.com" "roles/owner" + ) 2>&1 | indent + color 6 "Enabling APIs necessary for kubernetes e2e jobs to use e2e project: ${prj}" enable_api "${prj}" compute.googleapis.com enable_api "${prj}" logging.googleapis.com @@ -143,7 +153,7 @@ for prj; do --role roles/owner # NB: prow.viewer role is defined in ensure-organization.sh, that needs to have been run first - color 6 "Empower k8s-infra-prow-viewers@kubernetes.io to view e2e project: ${prj}" + color 6 "Empower k8s-infra-prow-viewers@kubernetes.io to view specific resources in e2e project: ${prj}" gcloud \ projects add-iam-policy-binding "${prj}" \ --member "group:k8s-infra-prow-viewers@kubernetes.io" \