IAM: Add validation for new tokens with no expiration when token_expiration_day_limit is set (#114166)

* do not create sa with no expiration when day limit is set

* disable No expiration option if day limit is set

* make i18n-extract

* run prettier

* address feedback
This commit is contained in:
Mihai Doarna
2025-11-20 13:44:15 +02:00
committed by GitHub
parent b2f022fb5e
commit fbadbf385d
4 changed files with 100 additions and 21 deletions
@@ -157,6 +157,10 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response.
}
if api.cfg.SATokenExpirationDayLimit > 0 {
if cmd.SecondsToLive == 0 {
return response.Error(http.StatusBadRequest, "Cannot create token with no expiration date when service_accounts.token_expiration_day_limit is set", nil)
}
dayExpireLimit := time.Now().Add(time.Duration(api.cfg.SATokenExpirationDayLimit) * time.Hour * 24).Truncate(24 * time.Hour)
expirationDate := time.Now().Add(time.Duration(cmd.SecondsToLive) * time.Second).Truncate(24 * time.Hour)
if expirationDate.After(dayExpireLimit) {
+55 -8
View File
@@ -60,14 +60,15 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) {
func TestServiceAccountsAPI_CreateToken(t *testing.T) {
type TestCase struct {
desc string
id int64
body string
permissions []accesscontrol.Permission
tokenTTL int64
expectedErr error
expectedAPIKey *apikey.APIKey
expectedCode int
desc string
id int64
body string
permissions []accesscontrol.Permission
tokenTTL int64
tokenExpirationDayLimit int
expectedErr error
expectedAPIKey *apikey.APIKey
expectedCode int
}
tests := []TestCase{
@@ -105,12 +106,58 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedCode: http.StatusBadRequest,
},
{
desc: "should not be able to create token for service account if max ttl is configured and exceeds the limit",
id: 1,
body: `{"name": "test", "secondsToLive": 11000}`,
tokenTTL: 10000,
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedCode: http.StatusBadRequest,
},
{
desc: "should be able to create token for service account if max ttl is configured and within the limit",
id: 1,
body: `{"name": "test", "secondsToLive": 5000}`,
tokenTTL: 10000,
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedAPIKey: &apikey.APIKey{},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to create token for service account if token expiration day limit is configured but not set in body",
id: 1,
body: `{"name": "test"}`,
tokenTTL: -1,
tokenExpirationDayLimit: 30,
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedCode: http.StatusBadRequest,
},
{
desc: "should not be able to create token for service account if token expiration day limit is configured and exceeds the limit",
id: 1,
body: `{"name": "test", "secondsToLive": 340000}`, // 340000 seconds is > 3 days
tokenTTL: -1,
tokenExpirationDayLimit: 3,
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedCode: http.StatusBadRequest,
},
{
desc: "should be able to create token for service account if token expiration day limit is configured and within the limit",
id: 1,
body: `{"name": "test", "secondsToLive": 250000}`, // 250000 seconds is almost 3 days
tokenTTL: -1,
tokenExpirationDayLimit: 3,
permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}},
expectedAPIKey: &apikey.APIKey{},
expectedCode: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
server := setupTests(t, func(a *ServiceAccountsAPI) {
a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL
a.cfg.SATokenExpirationDayLimit = tt.tokenExpirationDayLimit
a.service = &satests.FakeServiceAccountService{
ExpectedErr: tt.expectedErr,
ExpectedAPIKey: tt.expectedAPIKey,
@@ -16,10 +16,8 @@ import {
useStyles2,
} from '@grafana/ui';
const EXPIRATION_OPTIONS = [
{ label: 'No expiration', value: false },
{ label: 'Set expiration date', value: true },
];
const NO_EXPIRATION_OPTION = 'no-expiration';
const CUSTOM_EXPIRATION_OPTION = 'custom-expiration';
export type ServiceAccountToken = {
name: string;
@@ -44,11 +42,15 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
} else {
maxExpirationDate.setDate(8640000000000000);
}
const defaultExpirationDate = config.tokenExpirationDayLimit !== undefined && config.tokenExpirationDayLimit > 0;
const isTokenExpirationDayLimitConfigured =
config.tokenExpirationDayLimit !== undefined && config.tokenExpirationDayLimit > 0;
const defaultExpirationOption = isTokenExpirationDayLimitConfigured ? CUSTOM_EXPIRATION_OPTION : NO_EXPIRATION_OPTION;
const [defaultTokenName, setDefaultTokenName] = useState('');
const [newTokenName, setNewTokenName] = useState('');
const [isWithExpirationDate, setIsWithExpirationDate] = useState(defaultExpirationDate);
const [expirationOption, setExpirationOption] = useState(defaultExpirationOption);
const [newTokenExpirationDate, setNewTokenExpirationDate] = useState<Date | string>(tomorrow);
const [isExpirationDateValid, setIsExpirationDateValid] = useState(newTokenExpirationDate !== '');
const styles = useStyles2(getStyles);
@@ -69,14 +71,15 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
const onGenerateToken = () => {
onCreateToken({
name: newTokenName || defaultTokenName,
secondsToLive: isWithExpirationDate ? getSecondsToLive(newTokenExpirationDate) : undefined,
secondsToLive:
expirationOption === CUSTOM_EXPIRATION_OPTION ? getSecondsToLive(newTokenExpirationDate) : undefined,
});
};
const onCloseInternal = () => {
setNewTokenName('');
setDefaultTokenName('');
setIsWithExpirationDate(defaultExpirationDate);
setExpirationOption(defaultExpirationOption);
setNewTokenExpirationDate(tomorrow);
setIsExpirationDateValid(newTokenExpirationDate !== '');
onClose();
@@ -84,6 +87,24 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
const modalTitle = !token ? 'Add service account token' : 'Service account token created';
const getExpirationOptions = () => {
const noExpirationDescription = t(
'serviceaccounts.create-token-modal.description-no-expiration-disabled',
'Cannot create a token with no expiration date when token expiration day limit is configured'
);
return [
{
label: t('serviceaccounts.create-token-modal.label-no-expiration', 'No expiration'),
value: NO_EXPIRATION_OPTION,
description: isTokenExpirationDayLimitConfigured ? noExpirationDescription : undefined,
},
{
label: t('serviceaccounts.create-token-modal.label-set-expiration-date', 'Set expiration date'),
value: CUSTOM_EXPIRATION_OPTION,
},
];
};
return (
<Modal isOpen={isOpen} title={modalTitle} onDismiss={onCloseInternal} className={styles.modal}>
{!token ? (
@@ -109,13 +130,14 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
</Field>
<Field label={t('serviceaccounts.create-token-modal.label-expiration', 'Expiration')}>
<RadioButtonGroup
options={EXPIRATION_OPTIONS}
value={isWithExpirationDate}
onChange={setIsWithExpirationDate}
options={getExpirationOptions()}
disabledOptions={isTokenExpirationDayLimitConfigured ? [NO_EXPIRATION_OPTION] : []}
value={expirationOption}
onChange={setExpirationOption}
size="md"
/>
</Field>
{isWithExpirationDate && (
{expirationOption === CUSTOM_EXPIRATION_OPTION && (
<Field label={t('serviceaccounts.create-token-modal.label-expiration-date', 'Expiration date')}>
<DatePickerWithInput
onChange={onExpirationDateChange}
@@ -127,7 +149,10 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
</Field>
)}
<Modal.ButtonRow>
<Button onClick={onGenerateToken} disabled={isWithExpirationDate && !isExpirationDateValid}>
<Button
onClick={onGenerateToken}
disabled={expirationOption === CUSTOM_EXPIRATION_OPTION && !isExpirationDateValid}
>
<Trans i18nKey="serviceaccounts.create-token-modal.generate-token">Generate token</Trans>
</Button>
</Modal.ButtonRow>
+3
View File
@@ -12543,11 +12543,14 @@
"copy-clipboard": "Copy to clipboard",
"copy-to-clipboard-and-close": "Copy to clipboard and close",
"description-name-to-easily-identify-the-token": "Name to easily identify the token",
"description-no-expiration-disabled": "Cannot create a token with no expiration date when token expiration day limit is configured",
"description-token": "Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.",
"generate-token": "Generate token",
"label-display-name": "Display name",
"label-expiration": "Expiration",
"label-expiration-date": "Expiration date",
"label-no-expiration": "No expiration",
"label-set-expiration-date": "Set expiration date",
"label-token": "Token"
},
"get-actions-cell": {