Bigquery DataSet that contains a daily snapshot of all Standard Google Cloud IAM Roles and Permissions.
Its like this:
This is not an officially supported Google product
Here’s the dataset: https://console.cloud.google.com/bigquery?project=iam-log&p=iam-log&d=iam. To use this, first add the following project to the UI iam-log
. Once thats done, any query you issue will use the IAM dataset but bill your project for your own usage. …and no, i don’t know or care about the queries you run (eg, i don’t have any logging enabled)
with tables:
iam-log:iam.permissions
)bq show --format=prettyjson --schema iam-log:iam.permissions
[
{
"mode": "REQUIRED",
"name": "name",
"type": "STRING"
},
{
"mode": "REQUIRED",
"name": "region",
"type": "STRING"
},
{
"name": "title",
"type": "STRING"
},
{
"name": "description",
"type": "STRING"
},
{
"name": "stage",
"type": "STRING"
},
{
"name": "apiDisabled",
"type": "BOOLEAN"
},
{
"name": "customRolesSupportLevel",
"type": "STRING"
},
{
"name": "onlyInPredefinedRoles",
"type": "BOOLEAN"
},
{
"name": "primaryPermission",
"type": "STRING"
},
{
"mode": "REPEATED",
"name": "roles",
"type": "STRING"
}
]
iam-log:iam.roles
)bq show --format=prettyjson --schema iam-log:iam.roles
[
{
"mode": "REQUIRED",
"name": "name",
"type": "STRING"
},
{
"name": "title",
"type": "STRING"
},
{
"name": "stage",
"type": "STRING"
},
{
"name": "etag",
"type": "STRING"
},
{
"name": "deleted",
"type": "BOOLEAN"
},
{
"name": "description",
"type": "STRING"
},
{
"mode": "REPEATED",
"name": "included_permissions",
"type": "STRING"
},
{
"mode": "REQUIRED",
"name": "region",
"type": "STRING"
}
]
thats not a problem, you’re free to run your own (see the diy
section below).
Just modify the default cloud run image you deploy and set the mode
variable appropriately
organization
: This will iterate over all custom roles at the organization level. Remember to specify the organization
value in the startupproject
: This will iterate over all custom roles at each the project levelmode = flag.String("mode", "default", "Interation mode: organization|project|default")
organization = flag.String("organization", os.Getenv("ORGANIZATION_ID"), "OrganizationID")
Ofcourse, the cloud run SA you deploy will need access to view roles/permissions in your org (which is already granted through the IAM bindings below)
you can find the sourcecode here:
Here are some basic queries you can run
bq query --nouse_legacy_sql '
SELECT
_PARTITIONTIME AS pt,
FORMAT_TIMESTAMP("%Y%m%d", _PARTITIONTIME) AS partition_id
FROM
iam-log.iam.permissions
GROUP BY
_PARTITIONTIME
ORDER BY
_PARTITIONTIME
'
Get all Roles which includes storage.objects.get
permission on a given partition
bq query --nouse_legacy_sql '
SELECT
r1
FROM
iam-log.iam.permissions AS d1, UNNEST(roles) r1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d1.name = "storage.objects.get"
AND d1.region = "us-central1"
'
Get all Permissions included in roles/serverless.serviceAgent
permission on a given partition
bq query --nouse_legacy_sql '
SELECT
p1
FROM
iam-log.iam.roles AS d1,
UNNEST(included_permissions) p1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d1.name = "roles/serverless.serviceAgent"
AND d1.region = "us-central1"
'
bq query --nouse_legacy_sql '
SELECT
d1.name
FROM
iam-log.iam.roles AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-23")
AND d1.region = "us-central1"
AND d1.name NOT IN (
SELECT
d2.name
FROM
iam-log.iam.roles AS d2
WHERE
d2._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d2.region = "us-central1")
'
bq query --nouse_legacy_sql '
SELECT
d1.name
FROM
iam-log.iam.permissions AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-23")
AND d1.region = "us-central1"
AND d1.name NOT IN (
SELECT
d2.name
FROM
iam-log.iam.permissions AS d2
WHERE
d2._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d2.region = "us-central1")
'
IAM permissions and roles rollout gradually in different regions. The permission/role set retrieved by cloud run on any given day will reflect the state/view at that cell that cloud run connected to to retrieved the roles/permissions.
To (somewhat imprecisely) account for this, this script deploys about several different cloud run instances in different Cloud Run Regions globally. The idea is that each cloud run instance that is invoked will query the IAM Roles active sets within a region and then from the Role Response populate the list of permissions. However, a role may not found within that region which will omit any permissions that maybe visible. To account for that, the permission list is retrieved first using a iam.queryTestablePermissions() API call against the root organization resource (//cloudresourcemanager.googleapis.com/organizations/
)
For each Role thats found using IAM List Roles API call is invoked in each region the permissions back to what was found globally using queryTestablePermissions
It is ofcourse not a guarantee that the map reflects the region exactly since a CR instance can query a different region, but its likely that it stays in region and reflects a local map. This script also just iterates over the following regions that are globally available. If needed, tune this to your needs for wherever you deploy within your organization
export REGIONS=asia-east1,asia-northeast1,asia-northeast2,europe-north1,europe-west1,europe-west4,us-central1,us-east1,us-east4,us-west1,asia-east2,asia-northeast3,asia-southeast1,asia-southeast2,asia-south1,asia-south2,australia-southeast1,australia-southeast2,europe-central2,europe-west2,europe-west3,europe-west6,northamerica-northeast1,northamerica-northeast2,southamerica-east1,us-west2,us-west3,us-west4
Consider an iteration between two regions here for us-west2
and us-east1
.
In us-west2
has more permissions visible while us-east1
was not able to find a specific role roles/recommender.bigQueryCapacityCommitmentsViewer
. (why the total number of roles is the same is describe a bit below.)
A 2021-09-22T11:50:44.221238Z done
A 2021-09-22T11:50:41.647731Z Uploading [4760] Permissions from region [us-west2]
A 2021-09-22T11:50:37.918094Z Uploading [882] Roles from region [us-west2]
A 2021-09-22T11:50:37.718748Z Generating BigQuery output
A 2021-09-22T11:50:19.405329Z Getting Default Roles/Permissions
A 2021-09-22T11:50:19.405305Z Found [4760] permissions on //cloudresourcemanager.googleapis.com/organizations/<organizationID>
A 2021-09-22T11:50:01.464765Z / called for region us-west2
A 2021-09-22T11:50:01.389747Z Starting Server..
A 2021-09-22T11:14:38.967673Z done
A 2021-09-22T11:14:36.605246Z Uploading [4778] Permissions from region [us-east1]
A 2021-09-22T11:14:32.656471Z Uploading [882] Roles from region [us-east1]
A 2021-09-22T11:14:32.524652Z Generating BigQuery output
A 2021-09-22T11:14:29.103389Z Error getting role name googleapi: Error 404: The role named roles/recommender.bigQueryCapacityCommitmentsViewer was not found., notFound
A 2021-09-22T11:14:14.319069Z Getting Default Roles/Permissions
A 2021-09-22T11:14:14.319052Z Found [4778] permissions on //cloudresourcemanager.googleapis.com/organizations/<organizationID>
A 2021-09-22T11:14:01.572271Z / called for region us-east1
A 2021-09-22T11:14:01.538825Z Starting Server..
Here is the query for the difference in permissions
bq query --nouse_legacy_sql '
SELECT
d1.name
FROM
iam-log.iam.permissions AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d1.region = "us-east1"
AND d1.name NOT IN (
SELECT
d2.name
FROM
iam-log.iam.permissions AS d2
WHERE
d2._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d2.region = "us-west2")
'
+---------------------------------------------+
| name |
+---------------------------------------------+
| integrations.securityIntegrationVers.list |
| integrations.securityExecutions.cancel |
| integrations.securityAuthConfigs.create |
| integrations.securityAuthConfigs.update |
| integrations.securityAuthConfigs.get |
| integrations.securityExecutions.get |
| integrations.securityIntegTempVers.get |
| integrations.securityAuthConfigs.list |
| integrations.securityIntegTempVers.list |
| integrations.securityIntegrations.list |
| integrations.securityIntegrationVers.create |
| integrations.securityAuthConfigs.delete |
| integrations.securityIntegrationVers.deploy |
| integrations.securityIntegrationVers.get |
| integrations.securityExecutions.list |
| integrations.securityIntegTempVers.create |
| integrations.securityIntegrations.invoke |
| integrations.securityIntegrationVers.update |
+---------------------------------------------+
For roles, consider the query below which lists the roles that are visible between
bq query --nouse_legacy_sql '
SELECT
d1.name
FROM
iam-log.iam.roles AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d1.region = "us-west2"
AND d1.name NOT IN (
SELECT
d2.name
FROM
iam-log.iam.roles AS d2
WHERE
d2._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d2.region = "us-east1")
'
+-----------------------------------------------------+
| name |
+-----------------------------------------------------+
| roles/recommender.bigQueryCapacityCommitmentsViewer |
+-----------------------------------------------------+
Confusingly enough, the total number of roles is the same on both regions…thats because us-east1
happens to have an extra role visible
bq query --nouse_legacy_sql '
SELECT
d1.name
FROM
iam-log.iam.roles AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d1.region = "us-east1"
AND d1.name NOT IN (
SELECT
d2.name
FROM
iam-log.iam.roles AS d2
WHERE
d2._PARTITIONTIME = TIMESTAMP("2021-09-22")
AND d2.region = "us-west2")
'
+---------------------------------------------+
| name |
+---------------------------------------------+
| roles/integrations.securityIntegrationAdmin |
+---------------------------------------------+
bq query --nouse_legacy_sql '
SELECT
d1.region,
COUNT(*) AS num_permissions
FROM
iam-log.iam.permissions AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
GROUP BY
d1.region
ORDER BY
num_permissions DESC, d1.region
'
+-------------------------+-----------------+
| region | num_permissions |
+-------------------------+-----------------+
| europe-central2 | 4778 |
| europe-north1 | 4778 |
| europe-west1 | 4778 |
| europe-west2 | 4778 |
| europe-west3 | 4778 |
| europe-west4 | 4778 |
| europe-west6 | 4778 |
| northamerica-northeast1 | 4778 |
| northamerica-northeast2 | 4778 |
| southamerica-east1 | 4778 |
| us-east1 | 4778 |
| us-east4 | 4778 |
| asia-east1 | 4760 |
| asia-east2 | 4760 |
| asia-northeast1 | 4760 |
| asia-northeast2 | 4760 |
| asia-northeast3 | 4760 |
| asia-south1 | 4760 |
| asia-south2 | 4760 |
| asia-southeast1 | 4760 |
| asia-southeast2 | 4760 |
| australia-southeast1 | 4760 |
| australia-southeast2 | 4760 |
| us-central1 | 4760 |
| us-west1 | 4760 |
| us-west2 | 4760 |
| us-west3 | 4760 |
| us-west4 | 4760 |
+-------------------------+-----------------+
bq query --nouse_legacy_sql '
SELECT
d1.region,
COUNT(*) AS num_roles
FROM
iam-log.iam.roles AS d1
WHERE
d1._PARTITIONTIME = TIMESTAMP("2021-09-22")
GROUP BY
d1.region
ORDER BY
num_roles DESC, d1.region
'
+-------------------------+-----------+
| region | num_roles |
+-------------------------+-----------+
| asia-east1 | 885 |
| us-west3 | 885 |
| asia-south2 | 884 |
| europe-north1 | 884 |
| europe-central2 | 883 |
| europe-west3 | 883 |
| europe-west6 | 883 |
| northamerica-northeast1 | 883 |
| asia-east2 | 882 |
| asia-northeast1 | 882 |
| asia-northeast2 | 882 |
| asia-northeast3 | 882 |
| asia-south1 | 882 |
| asia-southeast2 | 882 |
| australia-southeast1 | 882 |
| australia-southeast2 | 882 |
| europe-west1 | 882 |
| southamerica-east1 | 882 |
| us-central1 | 882 |
| us-east1 | 882 |
| us-west1 | 882 |
| us-west2 | 882 |
| europe-west2 | 881 |
| europe-west4 | 881 |
| northamerica-northeast2 | 881 |
| us-east4 | 881 |
| us-west4 | 881 |
| asia-southeast1 | 880 |
+-------------------------+-----------+
In this case, which permission is in roles/iap.tunnelResourceAccessor
but not in roles/editor
bq query --nouse_legacy_sql '
SELECT
name AS permission_name
FROM
iam-log.iam.permissions AS d1
WHERE
"roles/iap.tunnelResourceAccessor" IN UNNEST(roles)
AND "roles/editor" NOT IN UNNEST(roles)
AND d1.region="us-central1"
AND d1._PARTITIONTIME = TIMESTAMP("2021-09-23")
GROUP BY
name
'
+----------------------------------+
| permission_name |
+----------------------------------+
| iap.tunnelInstances.accessViaIAP |
+----------------------------------+
The following section details how to setup your own dataset:
These steps requires org-level privileges just to set cloud run’s service account the ability to read permissions/roles.
First pick a project
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
# get the organizationID:
$ gcloud organizations list
DISPLAY_NAME ID DIRECTORY_CUSTOMER_ID
esodemoapp2.com 673208786098 C023zw3x8
export ORGANIZATION_ID=673208786098
# enable some services
gcloud services enable run.googleapis.com
gcloud services enable cloudresourcemanager.googleapis.com
gcloud services enable bigquery.googleapis.com
gcloud services enable iam.googleapis.com
# Cloud Scheduler uses appengine apis
gcloud services enable appengine.googleapis.com
gcloud services enable cloudscheduler.googleapis.com
# these are the ones i ended up with
gcloud services list
NAME TITLE
appengine.googleapis.com App Engine Admin API
bigquery.googleapis.com BigQuery API
bigquerystorage.googleapis.com BigQuery Storage API
cloudapis.googleapis.com Google Cloud APIs
clouddebugger.googleapis.com Cloud Debugger API
cloudresourcemanager.googleapis.com Cloud Resource Manager API
cloudscheduler.googleapis.com Cloud Scheduler API
cloudtrace.googleapis.com Cloud Trace API
containerregistry.googleapis.com Container Registry API
datastore.googleapis.com Cloud Datastore API
iam.googleapis.com Identity and Access Management (IAM) API
iamcredentials.googleapis.com IAM Service Account Credentials API
logging.googleapis.com Cloud Logging API
monitoring.googleapis.com Cloud Monitoring API
pubsub.googleapis.com Cloud Pub/Sub API
run.googleapis.com Cloud Run Admin API
servicemanagement.googleapis.com Service Management API
serviceusage.googleapis.com Service Usage API
sql-component.googleapis.com Cloud SQL
storage-api.googleapis.com Google Cloud Storage JSON API
storage-component.googleapis.com Cloud Storage
storage.googleapis.com Cloud Storage API
# build and deploy your own app
docker build -t gcr.io/$PROJECT_ID/iam_audit .
docker push gcr.io/$PROJECT_ID/iam_audit
# this is the svc account cloud run runs as
gcloud iam service-accounts create iam-audit-account --display-name "IAM Audit Service Account"
# allow Cloud Run's root svc account the ability to impersonate and get tokens for your custom SA
gcloud iam service-accounts add-iam-policy-binding iam-audit-account@$PROJECT_ID.iam.gserviceaccount.com \
--member=serviceAccount:service-$PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com --role=roles/run.serviceAgent
export REGIONS=asia-east1,asia-northeast1,asia-northeast2,europe-north1,europe-west1,europe-west4,us-central1,us-east1,us-east4,us-west1,asia-east2,asia-northeast3,asia-southeast1,asia-southeast2,asia-south1,asia-south2,australia-southeast1,australia-southeast2,europe-central2,europe-west2,europe-west3,europe-west6,northamerica-northeast1,northamerica-northeast2,southamerica-east1,us-west2,us-west3,us-west4
./deploy_run.sh
Add IAM permissions to CloudRun to cloud run. These steps should be run as an admin
gcloud organizations add-iam-policy-binding $ORGANIZATION_ID \
--member=serviceAccount:iam-audit-account@$PROJECT_ID.iam.gserviceaccount.com --role=roles/iam.organizationRoleViewer
gcloud organizations add-iam-policy-binding $ORGANIZATION_ID \
--member=serviceAccount:iam-audit-account@$PROJECT_ID.iam.gserviceaccount.com --role=roles/orgpolicy.policyViewer
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:iam-audit-account@$PROJECT_ID.iam.gserviceaccount.com --role=roles/bigquery.jobUse
Since the $REGION
contains all available regions, there will be one Run instance per region
Note, you can also query the stdout of each invocation of Cloud Run from another public dataset which is a Logs->BQ Logging Export
run_googleapis_com_stdout
stdout of each invocation# this is the service account cloud scheduler runs as
gcloud iam service-accounts create schedulerunner --display-name="Task Schedule Runner"
# allow Cloud Schedulers's root svc account the ability to impersonate and get tokens for your custom SA
gcloud iam service-accounts add-iam-policy-binding schedulerunner@$PROJECT_ID.iam.gserviceaccount.com \
--member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-cloudscheduler.iam.gserviceaccount.com --role=roles/cloudscheduler.serviceAgent
./deploy_scheduler.sh
Note, if you trigger cloud run multiple times over a partition, duplicate roles/permissions will be added to BQ since its an append. To account for that, you could construct a BQ query to look for DISTINCT. (again, i don’t know enough sql to show that)
Since the $REGION
contains all available regions, there will be one Scheduler pointing to Cloud Run per per region
# configure each partition for 30days
bq --location=US mk --dataset --default_partition_expiration=2592000 iam
# export the permission set the BQ dataset has
bq show --format=prettyjson iam > /tmp/mydataset.json
edit /tmp/mydataset.json
, add (remember to actually replace $PROJECT_ID
)
{
"role": "roles/bigquery.admin",
"userByEmail": "iam-audit-account@$PROJECT_ID.iam.gserviceaccount.com"
},
bq update --source /tmp/mydataset.json $PROJECT_ID:iam
then sit, relax and wait here till 1am bora-bora time (yup, i took that picture)
This site supports webmentions. Send me a mention via this form.