feat(service ping): add additional resource counts (#10621)

# Which Problems Are Solved

Using the service ping, we want to have some additional insights to how
zitadel is configured. The current resource count report contains
already some amount of configured policies, such as the login_policy.
But we do not know if for example MFA is enforced.

# How the Problems Are Solved

- Added the following counts to the report:
  - service users per organization
  - MFA enforcements (though login policy)
  - Notification policies with password change option enabled
  - SCIM provisioned users (using user metadata)
- Since all of the above are conditional based on at least a column
inside a projection, a new `migration.CountTriggerConditional` has been
added, where a condition (column values) and an option to track updates
on that column should be considered for the count.
- For this to be possible, the following changes had to be made to the
existing sql resources:
- the `resource_name` has been added to unique constraint on the
`projection.resource_counts` table
- triggers have been added / changed to individually track `INSERT`,
`UPDATE`(s) and `DELETE` and be able to handle conditions
- an optional argument has been added to the
`projections.count_resource()` function to allow providing the
information to `UP` or `DOWN` count the resource on an update.

# Additional Changes

None

# Additional Context

- partially solves #10244 (reporting audit log retention limit will be
handled in #10245 directly)
- backport to v4.x
This commit is contained in:
Livio Spring
2025-09-08 16:30:03 +00:00
committed by GitHub
parent ef058c8de6
commit 2dbe21fb30
10 changed files with 691 additions and 28 deletions
+69 -15
View File
@@ -3,10 +3,12 @@ package setup
import (
"fmt"
"github.com/zitadel/zitadel/internal/api/scim/metadata"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/migration"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/serviceping"
)
// triggerSteps defines the repeatable migrations that set up triggers
@@ -19,14 +21,14 @@ func triggerSteps(db *database.DB) []migration.RepeatableMigration {
domain.CountParentTypeInstance,
projection.InstanceColumnID,
projection.InstanceColumnID,
"instance",
serviceping.ResourceCountInstance,
),
migration.DeleteParentCountsTrigger(db,
projection.OrgProjectionTable,
domain.CountParentTypeOrganization,
projection.OrgColumnInstanceID,
projection.OrgColumnID,
"organization",
serviceping.ResourceCountOrganization,
),
// Count triggers for all the resources
@@ -35,91 +37,143 @@ func triggerSteps(db *database.DB) []migration.RepeatableMigration {
domain.CountParentTypeInstance,
projection.OrgColumnInstanceID,
projection.OrgColumnInstanceID,
"organization",
serviceping.ResourceCountOrganization,
),
migration.CountTrigger(db,
projection.ProjectProjectionTable,
domain.CountParentTypeOrganization,
projection.ProjectColumnInstanceID,
projection.ProjectColumnResourceOwner,
"project",
serviceping.ResourceCountProject,
),
migration.CountTrigger(db,
projection.UserTable,
domain.CountParentTypeOrganization,
projection.UserInstanceIDCol,
projection.UserResourceOwnerCol,
"user",
serviceping.ResourceCountUser,
),
migration.CountTriggerConditional(db,
projection.UserTable,
domain.CountParentTypeOrganization,
projection.UserInstanceIDCol,
projection.UserResourceOwnerCol,
serviceping.ResourceCountUserMachine,
false, // the user type cannot change, so we do not need to track updates
&migration.TriggerCondition{
Column: projection.UserTypeCol,
// since we marshal the value into and from json,
// we directly use the float64 value to prevent issues with the comparison of the previous migration
Value: float64(2),
},
),
migration.CountTrigger(db,
projection.InstanceMemberProjectionTable,
domain.CountParentTypeInstance,
projection.MemberInstanceID,
projection.MemberResourceOwner,
"iam_admin",
serviceping.ResourceCountIAMAdmin,
),
migration.CountTrigger(db,
projection.IDPTable,
domain.CountParentTypeInstance,
projection.IDPInstanceIDCol,
projection.IDPInstanceIDCol,
"identity_provider",
serviceping.ResourceCountIdentityProvider,
),
migration.CountTrigger(db,
projection.IDPTemplateLDAPTable,
domain.CountParentTypeInstance,
projection.LDAPInstanceIDCol,
projection.LDAPInstanceIDCol,
"identity_provider_ldap",
serviceping.ResourceCountIdentityProviderLDAP,
),
migration.CountTrigger(db,
projection.ActionTable,
domain.CountParentTypeInstance,
projection.ActionInstanceIDCol,
projection.ActionInstanceIDCol,
"action_v1",
serviceping.ResourceCountActionV1,
),
migration.CountTrigger(db,
projection.ExecutionTable,
domain.CountParentTypeInstance,
projection.ExecutionInstanceIDCol,
projection.ExecutionInstanceIDCol,
"execution",
serviceping.ResourceCountActionExecution,
),
migration.CountTrigger(db,
fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix),
domain.CountParentTypeInstance,
projection.ExecutionTargetInstanceIDCol,
projection.ExecutionTargetInstanceIDCol,
"execution_target",
serviceping.ResourceCountActionExecutionTarget,
),
migration.CountTrigger(db,
projection.LoginPolicyTable,
domain.CountParentTypeInstance,
projection.LoginPolicyInstanceIDCol,
projection.LoginPolicyInstanceIDCol,
"login_policy",
serviceping.ResourceCountLoginPolicy,
),
migration.CountTriggerConditional(db,
projection.LoginPolicyTable,
domain.CountParentTypeInstance,
projection.LoginPolicyInstanceIDCol,
projection.LoginPolicyInstanceIDCol,
serviceping.ResourceCountEnforceMFA,
true,
&migration.OrCondition{
Conditions: []migration.TriggerCondition{
{Column: projection.LoginPolicyForceMFACol, Value: true},
{Column: projection.LoginPolicyForceMFALocalOnlyCol, Value: true},
},
},
),
migration.CountTrigger(db,
projection.PasswordComplexityTable,
domain.CountParentTypeInstance,
projection.ComplexityPolicyInstanceIDCol,
projection.ComplexityPolicyInstanceIDCol,
"password_complexity_policy",
serviceping.ResourceCountPasswordComplexityPolicy,
),
migration.CountTrigger(db,
projection.PasswordAgeTable,
domain.CountParentTypeInstance,
projection.AgePolicyInstanceIDCol,
projection.AgePolicyInstanceIDCol,
"password_expiry_policy",
serviceping.ResourceCountPasswordExpiryPolicy,
),
migration.CountTrigger(db,
projection.LockoutPolicyTable,
domain.CountParentTypeInstance,
projection.LockoutPolicyInstanceIDCol,
projection.LockoutPolicyInstanceIDCol,
"lockout_policy",
serviceping.ResourceCountLockoutPolicy,
),
migration.CountTriggerConditional(db,
projection.NotificationPolicyProjectionTable,
domain.CountParentTypeInstance,
projection.NotificationPolicyColumnInstanceID,
projection.NotificationPolicyColumnInstanceID,
serviceping.ResourceCountPasswordChangeNotification,
true,
&migration.TriggerCondition{
Column: projection.NotificationPolicyColumnPasswordChange,
Value: true,
},
),
migration.CountTriggerConditional(db,
projection.UserMetadataProjectionTable,
domain.CountParentTypeOrganization,
projection.UserMetadataColumnInstanceID,
projection.LockoutPolicyResourceOwnerCol,
serviceping.ResourceCountScimProvisionedUser,
false, // the key cannot change, so we do not need to track updates
&migration.TriggerCondition{
Column: projection.UserMetadataColumnKey,
Value: metadata.KeyEmails,
},
),
}
}