2023-03-15 16:00:52 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2020-10-29 23:47:34 +00:00
package vault
import (
"context"
2022-05-25 00:00:46 +00:00
"fmt"
2020-10-29 23:47:34 +00:00
"net/http"
2022-05-25 00:00:46 +00:00
"os"
2020-10-29 23:47:34 +00:00
"path"
"strings"
"time"
"github.com/hashicorp/vault/helper/timeutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// activityQueryPath is available in every namespace
func ( b * SystemBackend ) activityQueryPath ( ) * framework . Path {
return & framework . Path {
Pattern : "internal/counters/activity$" ,
2023-04-13 15:32:57 +00:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "report" ,
OperationSuffix : "counts" ,
} ,
2020-10-29 23:47:34 +00:00
Fields : map [ string ] * framework . FieldSchema {
2021-04-08 16:43:39 +00:00
"start_time" : {
2020-10-29 23:47:34 +00:00
Type : framework . TypeTime ,
Description : "Start of query interval" ,
} ,
2021-04-08 16:43:39 +00:00
"end_time" : {
2020-10-29 23:47:34 +00:00
Type : framework . TypeTime ,
Description : "End of query interval" ,
} ,
2022-06-15 22:41:31 +00:00
"limit_namespaces" : {
Type : framework . TypeInt ,
Default : 0 ,
Description : "Limit query output by namespaces" ,
} ,
2020-10-29 23:47:34 +00: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-01 23:15:59 +00:00
// monthlyActivityCountPath is available in every namespace
func ( b * SystemBackend ) monthlyActivityCountPath ( ) * framework . Path {
return & framework . Path {
2023-04-13 15:32:57 +00:00
Pattern : "internal/counters/activity/monthly$" ,
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "report" ,
OperationSuffix : "counts-this-month" ,
} ,
2021-03-01 23:15:59 +00: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-08 23:38:35 +00:00
func ( b * SystemBackend ) activityPaths ( ) [ ] * framework . Path {
return [ ] * framework . Path {
b . monthlyActivityCountPath ( ) ,
b . activityQueryPath ( ) ,
}
}
2020-10-29 23:47:34 +00:00
// rootActivityPaths are available only in the root namespace
func ( b * SystemBackend ) rootActivityPaths ( ) [ ] * framework . Path {
2023-04-12 16:26:26 +00:00
paths := [ ] * framework . Path {
2020-10-29 23:47:34 +00:00
b . activityQueryPath ( ) ,
2021-03-01 23:15:59 +00:00
b . monthlyActivityCountPath ( ) ,
2020-10-29 23:47:34 +00:00
{
Pattern : "internal/counters/config$" ,
2023-04-13 15:32:57 +00:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
} ,
2020-10-29 23:47:34 +00:00
Fields : map [ string ] * framework . FieldSchema {
2020-12-15 19:11:28 +00:00
"default_report_months" : {
2020-10-29 23:47:34 +00:00
Type : framework . TypeInt ,
Default : 12 ,
Description : "Number of months to report if no start date specified." ,
} ,
2020-12-15 19:11:28 +00:00
"retention_months" : {
2020-10-29 23:47:34 +00:00
Type : framework . TypeInt ,
Default : 24 ,
Description : "Number of months of client data to retain. Setting to 0 will clear all existing data." ,
} ,
2020-12-15 19:11:28 +00:00
"enabled" : {
2020-10-29 23:47:34 +00: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 15:32:57 +00:00
DisplayAttrs : & framework . DisplayAttributes {
OperationVerb : "read" ,
OperationSuffix : "configuration" ,
} ,
Summary : "Read the client count tracking configuration." ,
2020-10-29 23:47:34 +00:00
} ,
logical . UpdateOperation : & framework . PathOperation {
Callback : b . handleActivityConfigUpdate ,
2023-04-13 15:32:57 +00:00
DisplayAttrs : & framework . DisplayAttributes {
OperationVerb : "configure" ,
} ,
Summary : "Enable or disable collection of client count, set retention period, or set default reporting period." ,
2020-10-29 23:47:34 +00:00
} ,
} ,
} ,
2022-05-25 00:00:46 +00:00
{
Pattern : "internal/counters/activity/export$" ,
2023-04-13 15:32:57 +00:00
DisplayAttrs : & framework . DisplayAttributes {
OperationPrefix : "internal-client-activity" ,
OperationVerb : "export" ,
} ,
2022-05-25 00:00:46 +00: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 15:32:57 +00:00
2022-05-25 00:00:46 +00:00
HelpSynopsis : strings . TrimSpace ( sysHelp [ "activity-export" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( sysHelp [ "activity-export" ] [ 1 ] ) ,
2020-10-29 23:47:34 +00:00
2022-05-25 00:00:46 +00: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-29 23:47:34 +00:00
}
2023-04-12 16:26:26 +00:00
if writePath := b . activityWritePath ( ) ; writePath != nil {
paths = append ( paths , writePath )
}
return paths
2022-05-25 00:00:46 +00:00
}
2020-10-29 23:47:34 +00:00
2022-05-25 00:00:46 +00:00
func parseStartEndTimes ( a * ActivityLog , d * framework . FieldData ) ( time . Time , time . Time , error ) {
2020-10-29 23:47:34 +00: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 15:58:45 +00:00
endTime = timeutil . EndOfMonth ( timeutil . StartOfPreviousMonth ( time . Now ( ) . UTC ( ) ) )
2020-10-29 23:47:34 +00:00
} else {
endTime = endTime . UTC ( )
}
if startTime . IsZero ( ) {
startTime = a . DefaultStartTime ( endTime )
} else {
startTime = startTime . UTC ( )
}
if startTime . After ( endTime ) {
2022-05-25 00:00:46 +00:00
return time . Time { } , time . Time { } , fmt . Errorf ( "start_time is later than end_time" )
}
return startTime , endTime , nil
}
2022-12-17 00:02:42 +00:00
// This endpoint is not used by the UI. The UI's "export" feature is entirely client-side.
2022-05-25 00:00:46 +00:00
func ( b * SystemBackend ) handleClientExport ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
a := b . Core . activityLog
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 != "" {
d , err := time . ParseDuration ( durationRaw )
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 ) {
a := b . Core . activityLog
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
2020-10-29 23:47:34 +00:00
}
2022-06-15 22:41:31 +00: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-29 23:47:34 +00: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-01 23:15:59 +00:00
func ( b * SystemBackend ) handleMonthlyActivityCount ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
a := b . Core . activityLog
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2021-09-07 16:16:12 +00:00
results , err := a . partialMonthClientCount ( ctx )
if err != nil {
return nil , err
}
2021-03-01 23:15:59 +00:00
if results == nil {
return logical . RespondWithStatusCode ( nil , req , http . StatusNoContent )
}
return & logical . Response {
Data : results ,
} , nil
}
2020-10-29 23:47:34 +00:00
func ( b * SystemBackend ) handleActivityConfigRead ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
a := b . Core . activityLog
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
config , err := a . loadConfigOrDefault ( ctx )
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-12 16:02:28 +00:00
"default_report_months" : config . DefaultReportMonths ,
"retention_months" : config . RetentionMonths ,
"enabled" : config . Enabled ,
"queries_available" : qa ,
"reporting_enabled" : b . Core . censusLicensingEnabled ,
"billing_start_timestamp" : b . Core . GetBillingStart ( ) ,
2020-10-29 23:47:34 +00:00
} ,
} , nil
}
func ( b * SystemBackend ) handleActivityConfigUpdate ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
a := b . Core . activityLog
if a == nil {
return logical . ErrorResponse ( "no activity log present" ) , nil
}
2020-12-15 19:11:28 +00:00
warnings := make ( [ ] string , 0 )
2020-10-29 23:47:34 +00:00
config , err := a . loadConfigOrDefault ( ctx )
if err != nil {
return nil , err
}
{
// 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
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 18:01:28 +00:00
if config . RetentionMonths > 36 {
config . RetentionMonths = 36
warnings = append ( warnings , "retention_months cannot be greater than 36; capped to 36." )
}
2020-10-29 23:47:34 +00:00
}
{
// Parse the enabled setting
if enabledRaw , ok := d . GetOk ( "enabled" ) ; ok {
2020-12-15 19:11:28 +00:00
enabledStr := enabledRaw . ( string )
2020-12-18 18:20:32 +00: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 19:11:28 +00:00
if config . Enabled == "enable" && enabledStr == "disable" ||
! activityLogEnabledDefault && config . Enabled == "enable" && enabledStr == "default" ||
activityLogEnabledDefault && config . Enabled == "default" && enabledStr == "disable" {
2023-03-31 15:05:16 +00:00
// if census is enabled, the activity log cannot be disabled
if a . core . censusLicensingEnabled {
return logical . ErrorResponse ( "cannot disable the activity log while Reporting is enabled" ) , logical . ErrInvalidRequest
}
2020-12-15 19:11:28 +00:00
warnings = append ( warnings , "the current monthly segment will be deleted because the activity log was disabled" )
}
2020-12-18 18:20:32 +00: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-29 23:47:34 +00: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
}
2023-04-11 15:09:01 +00:00
if a . core . censusLicensingEnabled && config . RetentionMonths < a . configOverrides . MinimumRetentionMonths {
return logical . ErrorResponse ( "retention_months must be at least %d while Reporting is enabled" , a . configOverrides . MinimumRetentionMonths ) , logical . ErrInvalidRequest
}
2020-10-29 23:47:34 +00: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 )
2020-12-15 19:11:28 +00:00
if len ( warnings ) > 0 {
return & logical . Response {
Warnings : warnings ,
} , nil
}
2020-10-29 23:47:34 +00:00
return nil , nil
}