Enforce PAT expiry for free-tier service accounts
Summary
Per the machine identity design document, free tier service accounts must always have PAT expiration enforced (unlike Premium/Ultimate where it's configurable by admins/group owners).
This MR adds the enforcement to ensure free-tier users cannot create non-expiring tokens for their service accounts.
Note: This MR is a blocker before the
service_accounts_available_on_free_or_unlicensedfeature flag can be enabled. See !225913 (merged) for the main enablement MR.
Changes
-
Authn::ServiceAccounts.free_tier?: New method (self-managed) - returnstruewhen SAs are available only via the feature flag (not via paid license) -
Authn::ServiceAccounts.free_tier_namespace?: New method (SaaS) - returnstruewhen namespace is on free tier but has SAs via the feature flag -
ServiceAccountTokenValidator#expiry_enforced?: Now always returnstruefor free tier, regardless of admin/group settings
Behavior
| Tier | PAT Expiry | Configurable? |
|---|---|---|
| Premium/Ultimate | Default on, admin/group owner can disable | Yes |
| Free + FF | Always on | No |
| Trial | Follows paid path | Yes |
How to validate
Prerequisites
- GDK running with EE license
Self-Managed (Free/Unlicensed Instance)
# 1. Open Rails console
bundle exec rails console
# 2. Enable the feature flag
Feature.enable(:service_accounts_available_on_free_or_unlicensed)
# 3. Verify no license or use starter license
puts "License: #{License.current&.plan}"
# 4. Disable the expiry setting at instance level
ApplicationSetting.current.update!(service_access_tokens_expiration_enforced: false)
# 5. Create or find a service account
sa = User.service_accounts.first
# 6. Verify free_tier? detection
puts "free_tier?: #{Authn::ServiceAccounts.free_tier?}"
# Expected: true
# 7. Test the validator - should ALWAYS return true on free tier
validator = EE::Gitlab::PersonalAccessTokens::ServiceAccountTokenValidator.new(sa)
puts "expiry_enforced?: #{validator.expiry_enforced?}"
# Expected: true (regardless of the setting being false)
SaaS (Free-Tier Namespace)
# 1. Open Rails console with SaaS enabled
GITLAB_SIMULATE_SAAS=1 bundle exec rails console
# 2. Enable namespace plan checking (required for SaaS simulation)
ApplicationSetting.current.update!(check_namespace_plan: true)
# 3. Get your group
group = Group.find_by_path('<your-group-path>')
# 4. Remove subscription to simulate free tier
group.gitlab_subscription&.destroy
group.clear_memoization(:features_available_in_plan)
group.clear_memoization(:licensed_feature_available)
group.reload
# 5. Enable the feature flag for this group
Feature.enable(:service_accounts_available_on_free_or_unlicensed, group)
# 6. Verify free tier detection
puts "feature_available_non_trial?: #{group.feature_available_non_trial?(:service_accounts)}"
# Expected: false
puts "free_tier_namespace?: #{Authn::ServiceAccounts.free_tier_namespace?(group)}"
# Expected: true
# 7. Get or create a service account provisioned by this group
sa = User.service_accounts.where(provisioned_by_group_id: group.id).first
# 8. Disable the group-level expiry setting
group.namespace_settings.update!(service_access_tokens_expiration_enforced: false)
# 9. Test the validator - should ALWAYS return true on free tier
validator = EE::Gitlab::PersonalAccessTokens::ServiceAccountTokenValidator.new(sa)
puts "expiry_enforced?: #{validator.expiry_enforced?}"
# Expected: true (regardless of the group setting being false)
Compare with Paid Namespace (SaaS)
# 1. Create a premium subscription
GitlabSubscription.create!(
namespace: group,
hosted_plan: Plan.find_by(name: 'premium'),
seats: 10,
start_date: Date.today,
end_date: 1.year.from_now
)
# 2. Clear memoization cache
group.clear_memoization(:features_available_in_plan)
group.clear_memoization(:licensed_feature_available)
group.reload
# 3. Verify NOT free tier
puts "free_tier_namespace?: #{Authn::ServiceAccounts.free_tier_namespace?(group)}"
# Expected: false
# 4. Disable expiry setting and verify it's respected for paid tier
group.namespace_settings.update!(service_access_tokens_expiration_enforced: false)
validator = EE::Gitlab::PersonalAccessTokens::ServiceAccountTokenValidator.new(sa)
puts "expiry_enforced?: #{validator.expiry_enforced?}"
# Expected: false (respects group setting for paid tier)
Expected Behavior Summary
| Tier | PAT Expiry Setting | expiry_enforced? |
|---|---|---|
| Premium/Ultimate | ON | true |
| Premium/Ultimate | OFF |
false (respects setting) |
| Free + FF enabled | ON | true |
| Free + FF enabled | OFF |
true (always enforced) |
| Trial | OFF |
false (follows paid path) |
Running the Specs
bundle exec rspec ee/spec/lib/authn/service_accounts_spec.rb \
ee/spec/lib/ee/gitlab/personal_access_tokens/service_account_token_validator_spec.rb \
--format documentation
Related
- Blocks FF enablement for !225913 (merged)
- Part of &20439 (Allow SAs for Free tier)
- Design doc: Machine Identity - Further restrictions
Edited by Smriti Garg