1package vault
2
3import (
4	"context"
5	"net/http"
6	"path"
7	"strings"
8	"time"
9
10	"github.com/hashicorp/vault/helper/timeutil"
11	"github.com/hashicorp/vault/sdk/framework"
12	"github.com/hashicorp/vault/sdk/logical"
13)
14
15// activityQueryPath is available in every namespace
16func (b *SystemBackend) activityQueryPath() *framework.Path {
17	return &framework.Path{
18		Pattern: "internal/counters/activity$",
19		Fields: map[string]*framework.FieldSchema{
20			"start_time": {
21				Type:        framework.TypeTime,
22				Description: "Start of query interval",
23			},
24			"end_time": {
25				Type:        framework.TypeTime,
26				Description: "End of query interval",
27			},
28		},
29		HelpSynopsis:    strings.TrimSpace(sysHelp["activity-query"][0]),
30		HelpDescription: strings.TrimSpace(sysHelp["activity-query"][1]),
31
32		Operations: map[logical.Operation]framework.OperationHandler{
33			logical.ReadOperation: &framework.PathOperation{
34				Callback: b.handleClientMetricQuery,
35				Summary:  "Report the client count metrics, for this namespace and all child namespaces.",
36			},
37		},
38	}
39}
40
41// monthlyActivityCountPath is available in every namespace
42func (b *SystemBackend) monthlyActivityCountPath() *framework.Path {
43	return &framework.Path{
44		Pattern:         "internal/counters/activity/monthly",
45		HelpSynopsis:    strings.TrimSpace(sysHelp["activity-monthly"][0]),
46		HelpDescription: strings.TrimSpace(sysHelp["activity-monthly"][1]),
47		Operations: map[logical.Operation]framework.OperationHandler{
48			logical.ReadOperation: &framework.PathOperation{
49				Callback: b.handleMonthlyActivityCount,
50				Summary:  "Report the number of clients for this month, for this namespace and all child namespaces.",
51			},
52		},
53	}
54}
55
56// rootActivityPaths are available only in the root namespace
57func (b *SystemBackend) rootActivityPaths() []*framework.Path {
58	return []*framework.Path{
59		b.activityQueryPath(),
60		b.monthlyActivityCountPath(),
61		{
62			Pattern: "internal/counters/config$",
63			Fields: map[string]*framework.FieldSchema{
64				"default_report_months": {
65					Type:        framework.TypeInt,
66					Default:     12,
67					Description: "Number of months to report if no start date specified.",
68				},
69				"retention_months": {
70					Type:        framework.TypeInt,
71					Default:     24,
72					Description: "Number of months of client data to retain. Setting to 0 will clear all existing data.",
73				},
74				"enabled": {
75					Type:        framework.TypeString,
76					Default:     "default",
77					Description: "Enable or disable collection of client count: enable, disable, or default.",
78				},
79			},
80			HelpSynopsis:    strings.TrimSpace(sysHelp["activity-config"][0]),
81			HelpDescription: strings.TrimSpace(sysHelp["activity-config"][1]),
82			Operations: map[logical.Operation]framework.OperationHandler{
83				logical.ReadOperation: &framework.PathOperation{
84					Callback: b.handleActivityConfigRead,
85					Summary:  "Read the client count tracking configuration.",
86				},
87				logical.UpdateOperation: &framework.PathOperation{
88					Callback: b.handleActivityConfigUpdate,
89					Summary:  "Enable or disable collection of client count, set retention period, or set default reporting period.",
90				},
91			},
92		},
93	}
94}
95
96func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
97	a := b.Core.activityLog
98	if a == nil {
99		return logical.ErrorResponse("no activity log present"), nil
100	}
101
102	startTime := d.Get("start_time").(time.Time)
103	endTime := d.Get("end_time").(time.Time)
104
105	// If a specific endTime is used, then respect that
106	// otherwise we want to give the latest N months, so go back to the start
107	// of the previous month
108	//
109	// Also convert any user inputs to UTC to avoid
110	// problems later.
111	if endTime.IsZero() {
112		endTime = timeutil.EndOfMonth(timeutil.StartOfPreviousMonth(time.Now().UTC()))
113	} else {
114		endTime = endTime.UTC()
115	}
116	if startTime.IsZero() {
117		startTime = a.DefaultStartTime(endTime)
118	} else {
119		startTime = startTime.UTC()
120	}
121	if startTime.After(endTime) {
122		return logical.ErrorResponse("start_time is later than end_time"), nil
123	}
124
125	results, err := a.handleQuery(ctx, startTime, endTime)
126	if err != nil {
127		return nil, err
128	}
129	if results == nil {
130		resp204, err := logical.RespondWithStatusCode(nil, req, http.StatusNoContent)
131		return resp204, err
132	}
133
134	return &logical.Response{
135		Data: results,
136	}, nil
137}
138
139func (b *SystemBackend) handleMonthlyActivityCount(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
140	a := b.Core.activityLog
141	if a == nil {
142		return logical.ErrorResponse("no activity log present"), nil
143	}
144
145	results := a.partialMonthClientCount(ctx)
146	if results == nil {
147		return logical.RespondWithStatusCode(nil, req, http.StatusNoContent)
148	}
149
150	return &logical.Response{
151		Data: results,
152	}, nil
153}
154
155func (b *SystemBackend) handleActivityConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
156	a := b.Core.activityLog
157	if a == nil {
158		return logical.ErrorResponse("no activity log present"), nil
159	}
160
161	config, err := a.loadConfigOrDefault(ctx)
162	if err != nil {
163		return nil, err
164	}
165
166	qa, err := a.queriesAvailable(ctx)
167	if err != nil {
168		return nil, err
169	}
170
171	if config.Enabled == "default" {
172		config.Enabled = activityLogEnabledDefaultValue
173	}
174
175	return &logical.Response{
176		Data: map[string]interface{}{
177			"default_report_months": config.DefaultReportMonths,
178			"retention_months":      config.RetentionMonths,
179			"enabled":               config.Enabled,
180			"queries_available":     qa,
181		},
182	}, nil
183}
184
185func (b *SystemBackend) handleActivityConfigUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
186	a := b.Core.activityLog
187	if a == nil {
188		return logical.ErrorResponse("no activity log present"), nil
189	}
190
191	warnings := make([]string, 0)
192
193	config, err := a.loadConfigOrDefault(ctx)
194	if err != nil {
195		return nil, err
196	}
197
198	{
199		// Parse the default report months
200		if defaultReportMonthsRaw, ok := d.GetOk("default_report_months"); ok {
201			config.DefaultReportMonths = defaultReportMonthsRaw.(int)
202		}
203
204		if config.DefaultReportMonths <= 0 {
205			return logical.ErrorResponse("default_report_months must be greater than 0"), logical.ErrInvalidRequest
206		}
207	}
208
209	{
210		// Parse the retention months
211		if retentionMonthsRaw, ok := d.GetOk("retention_months"); ok {
212			config.RetentionMonths = retentionMonthsRaw.(int)
213		}
214
215		if config.RetentionMonths < 0 {
216			return logical.ErrorResponse("retention_months must be greater than or equal to 0"), logical.ErrInvalidRequest
217		}
218	}
219
220	{
221		// Parse the enabled setting
222		if enabledRaw, ok := d.GetOk("enabled"); ok {
223			enabledStr := enabledRaw.(string)
224
225			// If we switch from enabled to disabled, then we return a warning to the client.
226			// We have to keep the default state of activity log enabled in mind
227			if config.Enabled == "enable" && enabledStr == "disable" ||
228				!activityLogEnabledDefault && config.Enabled == "enable" && enabledStr == "default" ||
229				activityLogEnabledDefault && config.Enabled == "default" && enabledStr == "disable" {
230				warnings = append(warnings, "the current monthly segment will be deleted because the activity log was disabled")
231			}
232
233			switch enabledStr {
234			case "default", "enable", "disable":
235				config.Enabled = enabledStr
236			default:
237				return logical.ErrorResponse("enabled must be one of \"default\", \"enable\", \"disable\""), logical.ErrInvalidRequest
238			}
239		}
240	}
241
242	enabled := config.Enabled == "enable"
243	if !enabled && config.Enabled == "default" {
244		enabled = activityLogEnabledDefault
245	}
246
247	if enabled && config.RetentionMonths == 0 {
248		return logical.ErrorResponse("retention_months cannot be 0 while enabled"), logical.ErrInvalidRequest
249	}
250
251	// Store the config
252	entry, err := logical.StorageEntryJSON(path.Join(activitySubPath, activityConfigKey), config)
253	if err != nil {
254		return nil, err
255	}
256	if err := req.Storage.Put(ctx, entry); err != nil {
257		return nil, err
258	}
259
260	// Set the new config on the activity log
261	a.SetConfig(ctx, config)
262
263	if len(warnings) > 0 {
264		return &logical.Response{
265			Warnings: warnings,
266		}, nil
267	}
268
269	return nil, nil
270}
271