diff --git a/conf/defaults.ini b/conf/defaults.ini index 023d23a53cb..57b6abc9a10 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -435,6 +435,10 @@ user_invite_max_lifetime_duration = 24h # Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves. hidden_users = +[service_accounts] +# When set, Grafana will not allow the creation of tokens with expiry greater than this setting. +token_expiration_day_limit = + [auth] # Login cookie name login_cookie_name = grafana_session diff --git a/conf/sample.ini b/conf/sample.ini index aacf119127c..4bbd7bf97e7 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -435,6 +435,11 @@ # Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves. ; hidden_users = +[service_accounts] +# Service account maximum expiration date in days. +# When set, Grafana will not allow the creation of tokens with expiry greater than this setting. +; token_expiration_day_limit = + [auth] # Login cookie name ;login_cookie_name = grafana_session diff --git a/docs/sources/administration/service-accounts/index.md b/docs/sources/administration/service-accounts/index.md index 7ed85d7a5a1..26f9429f50f 100644 --- a/docs/sources/administration/service-accounts/index.md +++ b/docs/sources/administration/service-accounts/index.md @@ -89,6 +89,10 @@ You can create a service account token using the Grafana UI or via the API. For - Ensure you have permission to create and edit service accounts. By default, the organization administrator role is required to create and edit service accounts. For more information about user permissions, refer to [About users and permissions]({{< relref "../roles-and-permissions/#" >}}). +### Service account token expiration dates + +By default, service account tokens don't have an expiration date, meaning they won't expire at all. However, if `token_expiration_day_limit` is set to a value greater than 0, Grafana restricts the lifetime limit of new tokens to the configured value in days. + ### To add a token to a service account 1. Sign in to Grafana, then hover your cursor over **Configuration** (the gear icon) in the sidebar. diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index d92e8cab4cc..fa85ac4b2b2 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -142,6 +142,8 @@ export class GrafanaBootConfig implements GrafanaConfig { rudderstackSdkUrl: undefined; rudderstackConfigUrl: undefined; + tokenExpirationDayLimit: undefined; + constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView; diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index e39ce8157ce..5b8f9e100cd 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -16,6 +16,7 @@ export interface DatePickerProps { onChange: (value: Date) => void; value?: Date; minDate?: Date; + maxDate?: Date; } /** @public */ @@ -38,7 +39,7 @@ export const DatePicker = memo((props) => { DatePicker.displayName = 'DatePicker'; -const Body = memo(({ value, minDate, onChange }) => { +const Body = memo(({ value, minDate, maxDate, onChange }) => { const styles = useStyles2(getBodyStyles); return ( @@ -47,6 +48,7 @@ const Body = memo(({ value, minDate, onChange }) => { tileClassName={styles.title} value={value || new Date()} minDate={minDate} + maxDate={maxDate} nextLabel={} prevLabel={} onChange={(ev: Date | Date[]) => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index 129c47a254e..a9efa07a766 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -15,6 +15,8 @@ export interface DatePickerWithInputProps extends Omit void; /** Hide the calendar when date is selected */ @@ -27,6 +29,7 @@ export interface DatePickerWithInputProps extends Omit { onChange(ev); if (closeOnSelect) { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 855a9564243..1db72e6ef2e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -198,9 +198,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "unifiedAlerting": map[string]interface{}{ "minInterval": hs.Cfg.UnifiedAlerting.MinInterval.String(), }, - "oauth": hs.getEnabledOAuthProviders(), - "samlEnabled": hs.samlEnabled(), - "samlName": hs.samlName(), + "oauth": hs.getEnabledOAuthProviders(), + "samlEnabled": hs.samlEnabled(), + "samlName": hs.samlName(), + "tokenExpirationDayLimit": hs.Cfg.SATokenExpirationDayLimit, } if hs.ThumbService != nil { diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index c1f31191dbd..2327422c7eb 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -160,6 +160,14 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon } } + if api.cfg.SATokenExpirationDayLimit > 0 { + 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) { + return response.Respond(http.StatusBadRequest, "The expiration date input exceeds the limit for service account access tokens expiration date") + } + } + newKeyInfo, err := apikeygenprefix.New(ServiceID) if err != nil { return response.Error(http.StatusInternalServerError, "Generating service account token failed", err) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 546974bbe8c..dc874143e40 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -385,6 +385,9 @@ type Cfg struct { HiddenUsers map[string]struct{} CaseInsensitiveLogin bool // Login and Email will be considered case insensitive + // Service Accounts + SATokenExpirationDayLimit int + // Annotations AnnotationCleanupJobBatchSize int64 AnnotationMaximumTagsLength int64 @@ -978,6 +981,9 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { if err := readUserSettings(iniFile, cfg); err != nil { return err } + if err := readServiceAccountSettings(iniFile, cfg); err != nil { + return err + } if err := readAuthSettings(iniFile, cfg); err != nil { return err } @@ -1481,6 +1487,12 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error { return nil } +func readServiceAccountSettings(iniFile *ini.File, cfg *Cfg) error { + serviceAccount := iniFile.Section("service_accounts") + cfg.SATokenExpirationDayLimit = serviceAccount.Key("token_expiration_day_limit").MustInt(-1) + return nil +} + func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { renderSec := iniFile.Section("rendering") cfg.RendererUrl = valueAsString(renderSec, "server_url", "") diff --git a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx index 73bb9d23ace..d5f0f4707f5 100644 --- a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx +++ b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Button, ClipboardButton, @@ -33,12 +34,18 @@ interface Props { } export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateToken, onClose }: Props) => { - let tomorrow = new Date(); + const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); + const maxExpirationDate = new Date(); + if (config.tokenExpirationDayLimit !== undefined) { + maxExpirationDate.setDate(maxExpirationDate.getDate() + config.tokenExpirationDayLimit + 1); + } + const defaultExpirationDate = config.tokenExpirationDayLimit !== undefined && config.tokenExpirationDayLimit > 0; + const [defaultTokenName, setDefaultTokenName] = useState(''); const [newTokenName, setNewTokenName] = useState(''); - const [isWithExpirationDate, setIsWithExpirationDate] = useState(false); + const [isWithExpirationDate, setIsWithExpirationDate] = useState(defaultExpirationDate); const [newTokenExpirationDate, setNewTokenExpirationDate] = useState(tomorrow); const [isExpirationDateValid, setIsExpirationDateValid] = useState(newTokenExpirationDate !== ''); const styles = useStyles2(getStyles); @@ -66,7 +73,7 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT const onCloseInternal = () => { setNewTokenName(''); setDefaultTokenName(''); - setIsWithExpirationDate(false); + setIsWithExpirationDate(defaultExpirationDate); setNewTokenExpirationDate(tomorrow); setIsExpirationDateValid(newTokenExpirationDate !== ''); onClose(); @@ -100,14 +107,16 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT }} /> - - - + {!isWithExpirationDate && ( + + + + )} {isWithExpirationDate && ( )}