2023-03-15 19:00:52 +03:00
// Copyright (c) HashiCorp, Inc.
2024-01-02 21:36:20 +03:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-15 19:00:52 +03:00
2020-10-30 02:47:34 +03:00
package vault
import (
"context"
2022-05-25 03:00:46 +03:00
"fmt"
2020-10-30 02:47:34 +03:00
"net/http"
2022-05-25 03:00:46 +03:00
"os"
2020-10-30 02:47:34 +03:00
"path"
"strings"
"time"
2023-06-21 17:01:13 +03:00
"github.com/hashicorp/go-secure-stdlib/parseutil"
2020-10-30 02:47:34 +03:00
"github.com/hashicorp/vault/helper/timeutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
2024-04-12 19:15:17 +03:00
// defaultToRetentionMonthsMaxWarning is a warning message for setting the max retention_months value when retention_months value is more than activityLogMaximumRetentionMonths
var defaultToRetentionMonthsMaxWarning = fmt . Sprintf ( "retention_months cannot be greater than %d; capped to %d." , activityLogMaximumRetentionMonths , activityLogMaximumRetentionMonths )
2020-10-30 02:47:34 +03:00
// activityQueryPath is available in every namespace
func ( b * SystemBackend ) activityQueryPath ( ) * framework . Path {
return & framework . Path {
Pattern : "internal/counters/activity$" ,
2023-04-13 18:32:57 +03:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "report" ,
OperationSuffix : "counts" ,
} ,
2020-10-30 02:47:34 +03:00
Fields : map [ string ] * framework . FieldSchema {
2023-05-22 16:22:45 +03:00
"current_billing_period" : {
Type : framework . TypeBool ,
Description : "Query utilization for configured billing period" ,
} ,
2021-04-08 19:43:39 +03:00
"start_time" : {
2020-10-30 02:47:34 +03:00
Type : framework . TypeTime ,
Description : "Start of query interval" ,
} ,
2021-04-08 19:43:39 +03:00
"end_time" : {
2020-10-30 02:47:34 +03:00
Type : framework . TypeTime ,
Description : "End of query interval" ,
} ,
2022-06-16 01:41:31 +03:00
"limit_namespaces" : {
Type : framework . TypeInt ,
Default : 0 ,
Description : "Limit query output by namespaces" ,
} ,
2020-10-30 02:47:34 +03:00
} ,
HelpSynopsis : strings . TrimSpace ( sysHelp [ "activity-query" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( sysHelp [ "activity-query" ] [ 1 ] ) ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleClientMetricQuery ,
Summary : "Report the client count metrics, for this namespace and all child namespaces." ,
} ,
} ,
}
}
2021-03-02 02:15:59 +03:00
// monthlyActivityCountPath is available in every namespace
func ( b * SystemBackend ) monthlyActivityCountPath ( ) * framework . Path {
return & framework . Path {
2023-04-13 18:32:57 +03:00
Pattern : "internal/counters/activity/monthly$" ,
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "report" ,
OperationSuffix : "counts-this-month" ,
} ,
2021-03-02 02:15:59 +03:00
HelpSynopsis : strings . TrimSpace ( sysHelp [ "activity-monthly" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( sysHelp [ "activity-monthly" ] [ 1 ] ) ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleMonthlyActivityCount ,
Summary : "Report the number of clients for this month, for this namespace and all child namespaces." ,
} ,
} ,
}
}
2021-11-09 02:38:35 +03:00
func ( b * SystemBackend ) activityPaths ( ) [ ] * framework . Path {
return [ ] * framework . Path {
b . monthlyActivityCountPath ( ) ,
b . activityQueryPath ( ) ,
}
}
2020-10-30 02:47:34 +03:00
// rootActivityPaths are available only in the root namespace
func ( b * SystemBackend ) rootActivityPaths ( ) [ ] * framework . Path {
2023-04-12 19:26:26 +03:00
paths := [ ] * framework . Path {
2020-10-30 02:47:34 +03:00
b . activityQueryPath ( ) ,
2021-03-02 02:15:59 +03:00
b . monthlyActivityCountPath ( ) ,
2020-10-30 02:47:34 +03:00
{
Pattern : "internal/counters/config$" ,
2023-04-13 18:32:57 +03:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
} ,
2020-10-30 02:47:34 +03:00
Fields : map [ string ] * framework . FieldSchema {
2020-12-15 22:11:28 +03:00
"default_report_months" : {
2020-10-30 02:47:34 +03:00
Type : framework . TypeInt ,
Default : 12 ,
Description : "Number of months to report if no start date specified." ,
} ,
2020-12-15 22:11:28 +03:00
"retention_months" : {
2020-10-30 02:47:34 +03:00
Type : framework . TypeInt ,
2024-04-12 19:15:17 +03:00
Default : ActivityLogMinimumRetentionMonths ,
2020-10-30 02:47:34 +03:00
Description : "Number of months of client data to retain. Setting to 0 will clear all existing data." ,
} ,
2020-12-15 22:11:28 +03:00
"enabled" : {
2020-10-30 02:47:34 +03:00
Type : framework . TypeString ,
Default : "default" ,
Description : "Enable or disable collection of client count: enable, disable, or default." ,
} ,
} ,
HelpSynopsis : strings . TrimSpace ( sysHelp [ "activity-config" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( sysHelp [ "activity-config" ] [ 1 ] ) ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleActivityConfigRead ,
2023-04-13 18:32:57 +03:00
DisplayAttrs : & framework . DisplayAttributes {
OperationVerb : "read" ,
OperationSuffix : "configuration" ,
} ,
Summary : "Read the client count tracking configuration." ,
2020-10-30 02:47:34 +03:00
} ,
logical . UpdateOperation : & framework . PathOperation {
Callback : b . handleActivityConfigUpdate ,
2023-04-13 18:32:57 +03:00
DisplayAttrs : & framework . DisplayAttributes {
OperationVerb : "configure" ,
} ,
Summary : "Enable or disable collection of client count, set retention period, or set default reporting period." ,
2020-10-30 02:47:34 +03:00
} ,
} ,
} ,
2022-05-25 03:00:46 +03:00
{
Pattern : "internal/counters/activity/export$" ,
2023-04-13 18:32:57 +03:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "export" ,
} ,
2022-05-25 03:00:46 +03:00
Fields : map [ string ] * framework . FieldSchema {
"start_time" : {
Type : framework . TypeTime ,
Description : "Start of query interval" ,
} ,
"end_time" : {
Type : framework . TypeTime ,
Description : "End of query interval" ,
} ,
"format" : {
Type : framework . TypeString ,
Description : "Format of the file. Either a CSV or a JSON file with an object per line." ,
Default : "json" ,
} ,
} ,
2023-04-13 18:32:57 +03:00
2022-05-25 03:00:46 +03:00
HelpSynopsis : strings . TrimSpace ( sysHelp [ "activity-export" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( sysHelp [ "activity-export" ] [ 1 ] ) ,
2020-10-30 02:47:34 +03:00
2022-05-25 03:00:46 +03:00
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleClientExport ,
Summary : "Report the client count metrics, for this namespace and all child namespaces." ,
} ,
} ,
} ,
2020-10-30 02:47:34 +03:00
}
2023-04-12 19:26:26 +03:00
if writePath := b . activityWritePath ( ) ; writePath != nil {
paths = append ( paths , writePath )
}
return paths
2022-05-25 03:00:46 +03:00
}
2020-10-30 02:47:34 +03:00
2022-05-25 03:00:46 +03:00
func parseStartEndTimes ( a * ActivityLog , d * framework . FieldData ) ( time . Time , time . Time , error ) {
2020-10-30 02:47:34 +03:00
startTime := d . Get ( "start_time" ) . ( time . Time )
endTime := d . Get ( "end_time" ) . ( time . Time )
// If a specific endTime is used, then respect that
// otherwise we want to give the latest N months, so go back to the start
// of the previous month
//
// Also convert any user inputs to UTC to avoid
// problems later.
if endTime . IsZero ( ) {
2021-03-30 18:58:45 +03:00
endTime = timeutil . EndOfMonth ( timeutil . StartOfPreviousMonth ( time . Now ( ) . UTC ( ) ) )
2020-10-30 02:47:34 +03:00
} else {
endTime = endTime . UTC ( )
}
if startTime . IsZero ( ) {
startTime = a . DefaultStartTime ( endTime )
} else {
startTime = startTime . UTC ( )
}
if startTime . After ( endTime ) {
2022-05-25 03:00:46 +03:00
return time . Time { } , time . Time { } , fmt . Errorf ( "start_time is later than end_time" )
}
return startTime , endTime , nil
}
2022-12-17 03:02:42 +03:00
// This endpoint is not used by the UI. The UI's "export" feature is entirely client-side.
2022-05-25 03:00:46 +03:00
func ( b * SystemBackend ) handleClientExport ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RLock ( )
2022-05-25 03:00:46 +03:00
a := b . Core . activityLog
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RUnlock ( )
2022-05-25 03:00:46 +03:00
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
startTime , endTime , err := parseStartEndTimes ( a , d )
if err != nil {
return logical . ErrorResponse ( err . Error ( ) ) , nil
}
// This is to avoid the default 90s context timeout.
timeout := 10 * time . Minute
if durationRaw := os . Getenv ( "VAULT_ACTIVITY_EXPORT_DURATION" ) ; durationRaw != "" {
2023-06-21 17:01:13 +03:00
d , err := parseutil . ParseDurationSecond ( durationRaw )
2022-05-25 03:00:46 +03:00
if err == nil {
timeout = d
}
}
runCtx , cancelFunc := context . WithTimeout ( b . Core . activeContext , timeout )
defer cancelFunc ( )
err = a . writeExport ( runCtx , req . ResponseWriter , d . Get ( "format" ) . ( string ) , startTime , endTime )
if err != nil {
return nil , err
}
return nil , nil
}
func ( b * SystemBackend ) handleClientMetricQuery ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2023-05-22 16:22:45 +03:00
var startTime , endTime time . Time
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RLock ( )
2022-05-25 03:00:46 +03:00
a := b . Core . activityLog
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RUnlock ( )
2022-05-25 03:00:46 +03:00
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2023-05-22 16:22:45 +03:00
if d . Get ( "current_billing_period" ) . ( bool ) {
startTime = b . Core . BillingStart ( )
endTime = time . Now ( ) . UTC ( )
} else {
var err error
startTime , endTime , err = parseStartEndTimes ( a , d )
if err != nil {
return logical . ErrorResponse ( err . Error ( ) ) , nil
}
2020-10-30 02:47:34 +03:00
}
2022-06-16 01:41:31 +03:00
var limitNamespaces int
if limitNamespacesRaw , ok := d . GetOk ( "limit_namespaces" ) ; ok {
limitNamespaces = limitNamespacesRaw . ( int )
}
results , err := a . handleQuery ( ctx , startTime , endTime , limitNamespaces )
2020-10-30 02:47:34 +03:00
if err != nil {
return nil , err
}
if results == nil {
resp204 , err := logical . RespondWithStatusCode ( nil , req , http . StatusNoContent )
return resp204 , err
}
return & logical . Response {
Data : results ,
} , nil
}
2021-03-02 02:15:59 +03:00
func ( b * SystemBackend ) handleMonthlyActivityCount ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RLock ( )
2021-03-02 02:15:59 +03:00
a := b . Core . activityLog
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RUnlock ( )
2021-03-02 02:15:59 +03:00
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2021-09-07 19:16:12 +03:00
results , err := a . partialMonthClientCount ( ctx )
if err != nil {
return nil , err
}
2021-03-02 02:15:59 +03:00
if results == nil {
return logical . RespondWithStatusCode ( nil , req , http . StatusNoContent )
}
return & logical . Response {
Data : results ,
} , nil
}
2020-10-30 02:47:34 +03:00
func ( b * SystemBackend ) handleActivityConfigRead ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RLock ( )
2020-10-30 02:47:34 +03:00
a := b . Core . activityLog
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RUnlock ( )
2020-10-30 02:47:34 +03:00
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2024-05-22 21:46:48 +03:00
config , err := a . loadConfigOrDefault ( ctx )
2020-10-30 02:47:34 +03:00
if err != nil {
return nil , err
}
qa , err := a . queriesAvailable ( ctx )
if err != nil {
return nil , err
}
if config . Enabled == "default" {
config . Enabled = activityLogEnabledDefaultValue
}
return & logical . Response {
Data : map [ string ] interface { } {
2023-04-13 21:33:23 +03:00
"default_report_months" : config . DefaultReportMonths ,
"retention_months" : config . RetentionMonths ,
"enabled" : config . Enabled ,
"queries_available" : qa ,
2024-02-02 02:03:55 +03:00
"reporting_enabled" : b . Core . AutomatedLicenseReportingEnabled ( ) ,
2023-05-19 17:42:50 +03:00
"billing_start_timestamp" : b . Core . BillingStart ( ) ,
2023-04-13 21:33:23 +03:00
"minimum_retention_months" : a . configOverrides . MinimumRetentionMonths ,
2020-10-30 02:47:34 +03:00
} ,
} , nil
}
func ( b * SystemBackend ) handleActivityConfigUpdate ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RLock ( )
2020-10-30 02:47:34 +03:00
a := b . Core . activityLog
2023-05-19 17:42:50 +03:00
b . Core . activityLogLock . RUnlock ( )
2020-10-30 02:47:34 +03:00
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2020-12-15 22:11:28 +03:00
warnings := make ( [ ] string , 0 )
2024-05-22 21:46:48 +03:00
config , err := a . loadConfigOrDefault ( ctx )
2020-10-30 02:47:34 +03:00
if err != nil {
return nil , err
}
2024-04-16 01:30:18 +03:00
prevRetentionMonths := config . RetentionMonths
2020-10-30 02:47:34 +03:00
{
// Parse the default report months
if defaultReportMonthsRaw , ok := d . GetOk ( "default_report_months" ) ; ok {
config . DefaultReportMonths = defaultReportMonthsRaw . ( int )
}
if config . DefaultReportMonths <= 0 {
return logical . ErrorResponse ( "default_report_months must be greater than 0" ) , logical . ErrInvalidRequest
}
}
{
// Parse the retention months
2024-04-12 19:15:17 +03:00
// For CE, this value can be between 0 and 60
// When reporting is enabled, this value can be between 48 and 60
2020-10-30 02:47:34 +03:00
if retentionMonthsRaw , ok := d . GetOk ( "retention_months" ) ; ok {
config . RetentionMonths = retentionMonthsRaw . ( int )
}
if config . RetentionMonths < 0 {
return logical . ErrorResponse ( "retention_months must be greater than or equal to 0" ) , logical . ErrInvalidRequest
}
2022-02-18 21:01:28 +03:00
2024-04-12 19:15:17 +03:00
if config . RetentionMonths > activityLogMaximumRetentionMonths {
config . RetentionMonths = activityLogMaximumRetentionMonths
warnings = append ( warnings , defaultToRetentionMonthsMaxWarning )
2022-02-18 21:01:28 +03:00
}
2020-10-30 02:47:34 +03:00
}
{
// Parse the enabled setting
if enabledRaw , ok := d . GetOk ( "enabled" ) ; ok {
2020-12-15 22:11:28 +03:00
enabledStr := enabledRaw . ( string )
2020-12-18 21:20:32 +03:00
// If we switch from enabled to disabled, then we return a warning to the client.
// We have to keep the default state of activity log enabled in mind
2020-12-15 22:11:28 +03:00
if config . Enabled == "enable" && enabledStr == "disable" ||
! activityLogEnabledDefault && config . Enabled == "enable" && enabledStr == "default" ||
activityLogEnabledDefault && config . Enabled == "default" && enabledStr == "disable" {
2023-03-31 18:05:16 +03:00
2024-02-02 02:03:55 +03:00
// if reporting is enabled, the activity log cannot be disabled. Manual Reporting is always enabled on ent.
if a . core . ManualLicenseReportingEnabled ( ) {
2023-03-31 18:05:16 +03:00
return logical . ErrorResponse ( "cannot disable the activity log while Reporting is enabled" ) , logical . ErrInvalidRequest
}
2020-12-15 22:11:28 +03:00
warnings = append ( warnings , "the current monthly segment will be deleted because the activity log was disabled" )
}
2020-12-18 21:20:32 +03:00
switch enabledStr {
case "default" , "enable" , "disable" :
config . Enabled = enabledStr
default :
return logical . ErrorResponse ( "enabled must be one of \"default\", \"enable\", \"disable\"" ) , logical . ErrInvalidRequest
}
2020-10-30 02:47:34 +03:00
}
}
2023-05-19 17:42:50 +03:00
a . core . activityLogLock . RLock ( )
minimumRetentionMonths := a . configOverrides . MinimumRetentionMonths
a . core . activityLogLock . RUnlock ( )
2020-10-30 02:47:34 +03:00
enabled := config . Enabled == "enable"
if ! enabled && config . Enabled == "default" {
enabled = activityLogEnabledDefault
}
if enabled && config . RetentionMonths == 0 {
return logical . ErrorResponse ( "retention_months cannot be 0 while enabled" ) , logical . ErrInvalidRequest
}
2024-04-12 19:15:17 +03:00
// if manual license reporting is enabled, retention months must at least be 48 months
2024-02-02 02:03:55 +03:00
if a . core . ManualLicenseReportingEnabled ( ) && config . RetentionMonths < minimumRetentionMonths {
2023-05-19 17:42:50 +03:00
return logical . ErrorResponse ( "retention_months must be at least %d while Reporting is enabled" , minimumRetentionMonths ) , logical . ErrInvalidRequest
2023-04-11 18:09:01 +03:00
}
2020-10-30 02:47:34 +03:00
// Store the config
entry , err := logical . StorageEntryJSON ( path . Join ( activitySubPath , activityConfigKey ) , config )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
// Set the new config on the activity log
a . SetConfig ( ctx , config )
2024-04-16 01:30:18 +03:00
// reload census agent if retention months change during update when reporting is enabled
if prevRetentionMonths != config . RetentionMonths {
2024-04-17 22:08:37 +03:00
if err := a . core . ReloadCensusActivityLog ( ) ; err != nil {
return nil , err
}
2024-04-16 01:30:18 +03:00
}
2020-12-15 22:11:28 +03:00
if len ( warnings ) > 0 {
return & logical . Response {
Warnings : warnings ,
} , nil
}
2020-10-30 02:47:34 +03:00
return nil , nil
}