mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 02:02:12 +08:00
Auth: Add expiry date for service accounts access tokens (#58885)
* Add new configuration option for SA tokens * Add new expiry date option to frontend components * Add backend validation Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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<DatePickerProps>((props) => {
|
||||
|
||||
DatePicker.displayName = 'DatePicker';
|
||||
|
||||
const Body = memo<DatePickerProps>(({ value, minDate, onChange }) => {
|
||||
const Body = memo<DatePickerProps>(({ value, minDate, maxDate, onChange }) => {
|
||||
const styles = useStyles2(getBodyStyles);
|
||||
|
||||
return (
|
||||
@ -47,6 +48,7 @@ const Body = memo<DatePickerProps>(({ value, minDate, onChange }) => {
|
||||
tileClassName={styles.title}
|
||||
value={value || new Date()}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
nextLabel={<Icon name="angle-right" />}
|
||||
prevLabel={<Icon name="angle-left" />}
|
||||
onChange={(ev: Date | Date[]) => {
|
||||
|
@ -15,6 +15,8 @@ export interface DatePickerWithInputProps extends Omit<InputProps, 'ref' | 'valu
|
||||
value?: Date | string;
|
||||
/** The minimum date the value can be set to */
|
||||
minDate?: Date;
|
||||
/** The maximum date the value can be set to */
|
||||
maxDate?: Date;
|
||||
/** Handles changes when a new date is selected */
|
||||
onChange: (value: Date | string) => void;
|
||||
/** Hide the calendar when date is selected */
|
||||
@ -27,6 +29,7 @@ export interface DatePickerWithInputProps extends Omit<InputProps, 'ref' | 'valu
|
||||
export const DatePickerWithInput = ({
|
||||
value,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
closeOnSelect,
|
||||
placeholder = 'Date',
|
||||
@ -56,6 +59,7 @@ export const DatePickerWithInput = ({
|
||||
isOpen={open}
|
||||
value={value && typeof value !== 'string' ? value : dateTime().toDate()}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
if (closeOnSelect) {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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", "")
|
||||
|
@ -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<Date | string>(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
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Expiration">
|
||||
<RadioButtonGroup
|
||||
options={EXPIRATION_OPTIONS}
|
||||
value={isWithExpirationDate}
|
||||
onChange={setIsWithExpirationDate}
|
||||
size="md"
|
||||
/>
|
||||
</Field>
|
||||
{!isWithExpirationDate && (
|
||||
<Field label="Expiration">
|
||||
<RadioButtonGroup
|
||||
options={EXPIRATION_OPTIONS}
|
||||
value={isWithExpirationDate}
|
||||
onChange={setIsWithExpirationDate}
|
||||
size="md"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{isWithExpirationDate && (
|
||||
<Field label="Expiration date">
|
||||
<DatePickerWithInput
|
||||
@ -115,6 +124,7 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT
|
||||
value={newTokenExpirationDate}
|
||||
placeholder=""
|
||||
minDate={tomorrow}
|
||||
maxDate={maxExpirationDate}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user