Merge pull request #4445 from hashicorp/llb-monthly-active-users

This adds support for tracking and reporting monthly active users for
the purpose of billing. It adds a new API endpoint,
`/v1/billing:monthly-active-users` and new cli command, `boundary
billing monthly-active-users` that can be used to view the monthly
active user counts.

This adds a new accumulating fact table to the data warehouse for auth
tokens. This fact table is used to calculate the number of active users
within a month, where an active user is a user that has at least one
issued auth token within the time range of the month.
This commit is contained in:
Timothy Messier authored and GitHub committed 2024-02-23 11:07:51 -05:00
commit 3856573eeb
84 files changed
+4903 -245

No files matched your search

+2
View File
@@ -244,6 +244,8 @@ protobuild:
@protoc-go-inject-tag -input=./internal/gen/controller/servers/servers.pb.go
@protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/policies/policy.pb.go
@protoc-go-inject-tag -input=./internal/gen/controller/api/services/policy_service.pb.go
@protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/billing/billing.pb.go
@protoc-go-inject-tag -input=./internal/gen/controller/api/services/billing_service.pb.go
# these protos, services and openapi artifacts are purely for testing purposes
+35
View File
@@ -0,0 +1,35 @@
// Code generated by "make api"; DO NOT EDIT.
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package billing
import (
"time"
"github.com/hashicorp/boundary/api"
)
type ActiveUsers struct {
Count uint32 `json:"count"`
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
}
// Client is a client for this collection
type Client struct {
client *api.Client
}
// Creates a new client for this collection. The submitted API client is cloned;
// modifications to it after generating this client will not have effect. If you
// need to make changes to the underlying API client, use ApiClient() to access
// it.
func NewClient(c *api.Client) *Client {
return &Client{client: c.Clone()}
}
// ApiClient returns the underlying API client
func (c *Client) ApiClient() *api.Client {
return c.client
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package billing
import (
"context"
"fmt"
"net/url"
"github.com/hashicorp/boundary/api"
)
type MonthlyActiveUsersResult struct {
Items []*ActiveUsers
response *api.Response
}
func (r MonthlyActiveUsersResult) GetItems() any {
return r.Items
}
func (r MonthlyActiveUsersResult) GetResponse() *api.Response {
return r.response
}
func (c *Client) MonthlyActiveUsers(ctx context.Context, opt ...Option) (*MonthlyActiveUsersResult, error) {
opts, apiOpts := getOpts(opt...)
if c.client == nil {
return nil, fmt.Errorf("nil client")
}
req, err := c.client.NewRequest(ctx, "GET", "billing:monthly-active-users", nil, apiOpts...)
if err != nil {
return nil, fmt.Errorf("error creating MonthlyActiveUsers request: %w", err)
}
if len(opts.queryMap) > 0 {
q := url.Values{}
for k, v := range opts.queryMap {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error performing client request during MonthlyActiveUsers call: %w", err)
}
mau := new(MonthlyActiveUsersResult)
mau.Items = []*ActiveUsers{}
apiErr, err := resp.Decode(mau)
if err != nil {
return nil, fmt.Errorf("error decoding MonthlyActiveUsers response: %w", err)
}
if apiErr != nil {
return nil, apiErr
}
mau.response = resp
return mau, nil
}
+104
View File
@@ -0,0 +1,104 @@
// Code generated by "make api"; DO NOT EDIT.
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package billing
import (
"fmt"
"strings"
"github.com/hashicorp/boundary/api"
)
// Option is a func that sets optional attributes for a call. This does not need
// to be used directly, but instead option arguments are built from the
// functions in this package. WithX options set a value to that given in the
// argument; DefaultX options indicate that the value should be set to its
// default. When an API call is made options are processed in the order they
// appear in the function call, so for a given argument X, a succession of WithX
// or DefaultX calls will result in the last call taking effect.
type Option func(*options)
type options struct {
postMap map[string]interface{}
queryMap map[string]string
withAutomaticVersioning bool
withSkipCurlOutput bool
withFilter string
withListToken string
}
func getDefaultOptions() options {
return options{
postMap: make(map[string]interface{}),
queryMap: make(map[string]string),
}
}
func getOpts(opt ...Option) (options, []api.Option) {
opts := getDefaultOptions()
for _, o := range opt {
if o != nil {
o(&opts)
}
}
var apiOpts []api.Option
if opts.withSkipCurlOutput {
apiOpts = append(apiOpts, api.WithSkipCurlOutput(true))
}
if opts.withFilter != "" {
opts.queryMap["filter"] = opts.withFilter
}
if opts.withListToken != "" {
opts.queryMap["list_token"] = opts.withListToken
}
return opts, apiOpts
}
// If set, and if the version is zero during an update, the API will perform a
// fetch to get the current version of the resource and populate it during the
// update call. This is convenient but opens up the possibility for subtle
// order-of-modification issues, so use carefully.
func WithAutomaticVersioning(enable bool) Option {
return func(o *options) {
o.withAutomaticVersioning = enable
}
}
// WithSkipCurlOutput tells the API to not use the current call for cURL output.
// Useful for when we need to look up versions.
func WithSkipCurlOutput(skip bool) Option {
return func(o *options) {
o.withSkipCurlOutput = true
}
}
// WithListToken tells the API to use the provided list token
// for listing operations on this resource.
func WithListToken(listToken string) Option {
return func(o *options) {
o.withListToken = listToken
}
}
// WithFilter tells the API to filter the items returned using the provided
// filter term. The filter should be in a format supported by
// hashicorp/go-bexpr.
func WithFilter(filter string) Option {
return func(o *options) {
o.withFilter = strings.TrimSpace(filter)
}
}
func WithEndTime(inEndTime string) Option {
return func(o *options) {
o.queryMap["end_time"] = fmt.Sprintf("%v", inEndTime)
}
}
func WithStartTime(inStartTime string) Option {
return func(o *options) {
o.queryMap["start_time"] = fmt.Sprintf("%v", inStartTime)
}
}
+37
View File
@@ -10,6 +10,7 @@ import (
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentialstores"
@@ -65,6 +66,7 @@ type fieldInfo struct {
Query bool
SkipDefault bool
JsonTags []string // Appended to a field's `json` tag (comma separated)
AllowEmpty bool
}
type structInfo struct {
@@ -138,6 +140,8 @@ type structInfo struct {
// fieldFilter is a set of field names that will not result in generated API
// fields
fieldFilter []string
allowEmpty bool
}
var inputStructs = []*structInfo{
@@ -224,6 +228,39 @@ var inputStructs = []*structInfo{
createResponseTypes: []string{CreateResponseType, ReadResponseType, UpdateResponseType, DeleteResponseType, ListResponseType},
recursiveListing: true,
},
{
inProto: &billing.ActiveUsers{},
outFile: "billing/active_users.gen.go",
templates: []*template.Template{
clientTemplate,
},
fieldOverrides: []fieldInfo{
{
Name: "Count",
ProtoName: "count",
FieldType: "uint32",
AllowEmpty: true,
},
},
extraFields: []fieldInfo{
{
Name: "StartTime",
ProtoName: "start_time",
FieldType: "string",
SkipDefault: true,
Query: true,
},
{
Name: "EndTime",
ProtoName: "end_time",
FieldType: "string",
SkipDefault: true,
Query: true,
},
},
pluralResourceName: "billing",
versionEnabled: true,
},
// User related resources
{
inProto: &users.Account{},
+4 -1
View File
@@ -94,6 +94,9 @@ func fillTemplates() {
if len(override.JsonTags) != 0 {
field.JsonTags = override.JsonTags
}
if override.AllowEmpty {
field.AllowEmpty = true
}
in.generatedStructure.fields[i] = field
}
}
@@ -715,7 +718,7 @@ import (
)
type {{ .Name }} struct { {{ range .Fields }}
{{ .Name }} {{ .FieldType }} `, "`json:\"{{ .ProtoName }}{{ if ( ne ( len ( .JsonTags ) ) 0 ) }},{{ stringsjoin .JsonTags \",\" }}{{ end }},omitempty\"`", `{{ end }}
{{ .Name }} {{ .FieldType }} `, "`json:\"{{ .ProtoName }}{{ if ( ne ( len ( .JsonTags ) ) 0 ) }},{{ stringsjoin .JsonTags \",\" }}{{ end }}{{ if ( not .AllowEmpty ) }},omitempty{{ end }}\"`", `{{ end }}
{{ if ( not ( eq ( len ( .CreateResponseTypes ) ) 0 ) )}}
response *api.Response
{{ else if ( eq .Name "Error" ) }}
+5 -1
View File
@@ -130,13 +130,17 @@ func TestAuthToken_DbCreate(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rootWrapper := db.TestWrapper(t)
iamRepo := iam.TestRepo(t, conn, rootWrapper)
kms := kms.TestKms(t, conn, rootWrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper))
org, _ := iam.TestScopes(t, iamRepo)
wrapper, err := kms.GetWrapper(context.Background(), org.GetPublicId(), 1)
require.NoError(t, err)
user := iam.TestUser(t, iamRepo, org.GetPublicId())
am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0]
acct := password.TestAccount(t, conn, am.GetPublicId(), "name1")
createdAuthToken := TestAuthToken(t, conn, kms, org.GetPublicId())
_, err = iamRepo.AddUserAccounts(ctx, user.GetPublicId(), user.GetVersion(), []string{acct.GetPublicId()})
require.NoError(t, err)
testAuthTokenId := func() string {
id, err := NewAuthTokenId(ctx)
+8 -1
View File
@@ -890,7 +890,9 @@ func Test_CloseExpiredPendingTokens(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper))
iamRepo := iam.TestRepo(t, conn, rootWrapper)
org, _ := iam.TestScopes(t, iamRepo)
user := iam.TestUser(t, iamRepo, org.GetPublicId())
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
@@ -900,6 +902,11 @@ func Test_CloseExpiredPendingTokens(t *testing.T) {
accts := password.TestMultipleAccounts(t, conn, authMethodId, cnt)
for i := 0; i < cnt; i++ {
user, _, err = iamRepo.LookupUser(ctx, user.GetPublicId())
require.NoError(t, err)
_, _ = iamRepo.AddUserAccounts(ctx, user.GetPublicId(), user.GetVersion(), []string{accts[i].GetPublicId()})
at := allocAuthToken()
id, err := NewAuthTokenId(ctx)
require.NoError(t, err)
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import "time"
// The ActiveUsersCount field is the number of unique users
// counted between the start and end dates.
// The start date is inclusive and the end date is exclusive.
type ActiveUsers struct {
StartTime time.Time
EndTime time.Time
ActiveUsersCount uint32
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
// Package billing provides usage numbers that can be used for
// billing purposes. The currently supported metric is monthly
// active users. A user is considered active within a month
// if they have at least one issued auth token within the time
// range of the start and end of a given month.
package billing
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"time"
)
// getOpts - iterate the inbound Options and return a struct
func getOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
}
return opts
}
// Option - how Options are passed as arguments.
type Option func(*options)
// options = how options are represented
type options struct {
withStartTime *time.Time
withEndTime *time.Time
}
func getDefaultOptions() options {
return options{
withStartTime: nil,
withEndTime: nil,
}
}
// WithStartTime allows setting the start time for the query.
func WithStartTime(startTime *time.Time) Option {
return func(o *options) {
o.withStartTime = startTime
}
}
// WithEndTime allows setting the end time for the query.
func WithEndTime(endTime *time.Time) Option {
return func(o *options) {
o.withEndTime = endTime
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
oneMonthAgo := time.Now().AddDate(0, -1, 0)
t.Run("WithStartTime", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts(WithStartTime(&oneMonthAgo))
testOpts := getDefaultOptions()
testOpts.withStartTime = &oneMonthAgo
assert.Equal(opts, testOpts)
})
t.Run("WithEndTime", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts(WithEndTime(&oneMonthAgo))
testOpts := getDefaultOptions()
testOpts.withEndTime = &oneMonthAgo
assert.Equal(opts, testOpts)
})
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
const (
activeUsersLastTwoMonthsQuery = `
select *
from hcp_billing_monthly_active_users_last_2_months();
`
activeUsersWithStartTimeQuery = `
select *
from hcp_billing_monthly_active_users_all(@start_time);
`
activeUsersWithStartTimeAndEndTimeQuery = `
select *
from hcp_billing_monthly_active_users_all(@start_time, @end_time);
`
)
+107
View File
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"database/sql"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
)
// A Repository retrieves the persistent type in the billing
// package. It is not safe to use a repository concurrently.
// It provides a method for requesting pre-aggregated user counts
// per month. Depending on whether a start time and/or end time are given,
// an ActiveUsers object will be returned:
// - for every month from the provided start date until the present date,
// with the present date being a cumulative count up to the present date.
// - for every month from the provided start date until the provided end date.
// - for the previous month and current month, with the current month being a
// cumulative count up to the present date.
type Repository struct {
reader db.Reader
}
// NewRepository creates a new Repository. The returned repository is not safe for concurrent go
// routines to access it.
func NewRepository(ctx context.Context, r db.Reader) (*Repository, error) {
const op = "billing.NewRepository"
switch {
case r == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil db reader")
}
return &Repository{
reader: r,
}, nil
}
// MonthlyActiveUsers returns the active users for a range of months, from most recent to least.
// If no start or end time is provided, it will return the active users for the last two months.
// If a start time is provided, it will return the active users for that month until the current month.
// If both a start and end time are provided, it will return the active users for that time range,
// starting time inclusive and ending time exclusive.
// The times provided must be the start of the month at midnight UTC.
func (r *Repository) MonthlyActiveUsers(ctx context.Context, opt ...Option) ([]ActiveUsers, error) {
const op = "billing.Repository.MonthlyActiveUsers"
opts := getOpts(opt...)
switch {
case opts.withEndTime != nil && opts.withStartTime == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "end time set without start time")
case opts.withEndTime != nil && !opts.withEndTime.After(*opts.withStartTime):
return nil, errors.New(ctx, errors.InvalidParameter, op, "start time is not before end time")
}
query := activeUsersLastTwoMonthsQuery
var args []any
if opts.withStartTime != nil {
if *opts.withStartTime != time.Date(opts.withStartTime.Year(), opts.withStartTime.Month(), 1, 0, 0, 0, 0, time.UTC) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "start time must be the first day of the month at midnight UTC")
}
query = activeUsersWithStartTimeQuery
args = append(args,
sql.Named("start_time", timestamp.New(*opts.withStartTime)))
}
if opts.withEndTime != nil {
if *opts.withEndTime != time.Date(opts.withEndTime.Year(), opts.withEndTime.Month(), 1, 0, 0, 0, 0, time.UTC) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "end time must be the first day of the month at midnight UTC")
}
query = activeUsersWithStartTimeAndEndTimeQuery
args = append(args,
sql.Named("end_time", timestamp.New(*opts.withEndTime)))
}
var activeUsers []ActiveUsers
rows, err := r.reader.Query(ctx, query, args)
if err != nil {
return nil, err
}
defer rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
for rows.Next() {
var startTime time.Time
var endTime time.Time
var count uint32
if err := rows.Scan(&startTime, &endTime, &count); err != nil {
return nil, err
}
// set start and end times to be in UTC
auUTC := ActiveUsers{
ActiveUsersCount: count,
StartTime: startTime.UTC(),
EndTime: endTime.UTC(),
}
activeUsers = append(activeUsers, auUTC)
}
return activeUsers, nil
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
)
func TestRepository_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
type args struct {
r db.Reader
w db.Writer
}
tests := []struct {
name string
args args
want *Repository
wantIsErr errors.Code
wantErrMsg string
}{
{
name: "valid",
args: args{
r: rw,
},
want: &Repository{
reader: rw,
},
},
{
name: "nil-reader",
args: args{
r: nil,
},
want: nil,
wantIsErr: errors.InvalidParameter,
wantErrMsg: "billing.NewRepository: nil db reader: parameter violation: error #100",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got, err := NewRepository(context.Background(), tt.args.r)
if tt.wantIsErr != 0 {
assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
assert.Equal(tt.wantErrMsg, err.Error())
return
}
assert.NoError(err)
assert.NotNil(got)
assert.Equal(tt.want, got)
})
}
}
func TestRepository_MonthlyActiveUsers(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
TestGenerateActiveUsers(t, conn)
today := time.Now().UTC()
threeMonthsAgo := time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
oneMonthAgo := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
midMonth := time.Date(today.Year(), today.Month(), 15, 0, 0, 0, 0, time.UTC)
t.Run("valid-no-options", func(t *testing.T) {
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx)
assert.NoError(t, err)
require.Len(t, activeUsers, 2)
// check counts for the last two months
require.Equal(t, uint32(0), activeUsers[0].ActiveUsersCount)
require.Equal(t, uint32(6), activeUsers[1].ActiveUsersCount)
// assert start and end times are correct
// the current month (contains the hour)
assert.Equal(t, time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC), activeUsers[0].StartTime)
assert.Equal(t, time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC), activeUsers[0].EndTime)
// the previous month
assert.Equal(t, time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC), activeUsers[1].StartTime)
assert.Equal(t, time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC), activeUsers[1].EndTime)
})
t.Run("valid-with-start-time", func(t *testing.T) {
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&threeMonthsAgo))
assert.NoError(t, err)
require.Len(t, activeUsers, 4)
for i := 0; i < 4; i++ {
// check counts for the last four months
if i == 0 {
assert.Equal(t, uint32(0), activeUsers[i].ActiveUsersCount)
// the current month (contains the hour)
assert.Equal(t, time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC), activeUsers[i].StartTime)
assert.Equal(t, time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC), activeUsers[i].EndTime)
} else {
// create a sliding window of dates to assert start and end times are correct
expectedStartTime := time.Date(today.AddDate(0, -i, 0).Year(), today.AddDate(0, -i, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
expectedEndTime := time.Date(today.AddDate(0, -i+1, 0).Year(), today.AddDate(0, -i+1, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
assert.Equal(t, uint32(6), activeUsers[i].ActiveUsersCount)
assert.Equal(t, expectedStartTime, activeUsers[i].StartTime)
assert.Equal(t, expectedEndTime, activeUsers[i].EndTime)
}
}
})
t.Run("valid-with-start-and-end-time", func(t *testing.T) {
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&threeMonthsAgo), WithEndTime(&oneMonthAgo))
assert.NoError(t, err)
require.Len(t, activeUsers, 2)
expectedStartTime := time.Date(today.AddDate(0, -2, 0).Year(), today.AddDate(0, -2, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
expectedEndTime := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
require.Equal(t, uint32(6), activeUsers[0].ActiveUsersCount)
assert.Equal(t, expectedStartTime, activeUsers[0].StartTime)
assert.Equal(t, expectedEndTime, activeUsers[0].EndTime)
expectedStartTime = time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
expectedEndTime = time.Date(today.AddDate(0, -2, 0).Year(), today.AddDate(0, -2, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
require.Equal(t, uint32(6), activeUsers[1].ActiveUsersCount)
assert.Equal(t, expectedStartTime, activeUsers[1].StartTime)
assert.Equal(t, expectedEndTime, activeUsers[1].EndTime)
})
t.Run("invalid-end-time-without-start-time", func(t *testing.T) {
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithEndTime(&oneMonthAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: end time set without start time: parameter violation: error #100", err.Error())
})
t.Run("invalid-end-time-before-start-time", func(t *testing.T) {
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&threeMonthsAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time is not before end time: parameter violation: error #100", err.Error())
})
t.Run("invalid-start-time-equals-end-time", func(t *testing.T) {
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&oneMonthAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time is not before end time: parameter violation: error #100", err.Error())
})
t.Run("invalid-start-time-not-first-day-of-month", func(t *testing.T) {
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&midMonth))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time must be the first day of the month at midnight UTC: parameter violation: error #100", err.Error())
})
t.Run("invalid-end-time-not-first-day-of-month", func(t *testing.T) {
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&midMonth))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: end time must be the first day of the month at midnight UTC: parameter violation: error #100", err.Error())
})
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/stretchr/testify/require"
)
const insertQuery = `
with
month_range (date_key, time_key, month) as (
select wh_date_key(s), wh_time_key(s), s
from generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
users (user_id, u) as (
select 'u_____user'||u, u
from generate_series(1, 6, 1) as u
),
user_key (key, user_id) as (
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email)
select users.user_id, 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
from users
returning key, user_id
),
tokens (date_key, time_key, user_id, token_id) as (
select wh_date_key(s), wh_time_key(s), users.user_id, 't_____u'||users.u||'tok'||s as token_id
from users,
generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
tokens_user_keys (date_key, time_key, user_id, token_id, user_key) as (
select tokens.date_key, tokens.time_key, tokens.user_id, tokens.token_id, user_key.key
from tokens
join user_key
on user_key.user_id = tokens.user_id
),
auth_tokens (user_key, user_id, token_id, valid_range) as (
select tokens_user_keys.user_key, tokens_user_keys.user_id, tokens_user_keys.token_id, tstzrange(month_range.month, month_range.month + interval '5 minutes', '[)')
from tokens_user_keys
join month_range
on month_range.date_key = tokens_user_keys.date_key
and month_range.time_key = tokens_user_keys.time_key
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select auth_tokens.token_id, auth_tokens.user_key,
wh_date_key(lower(auth_tokens.valid_range)), wh_time_key(lower(auth_tokens.valid_range)), lower(auth_tokens.valid_range),
coalesce(wh_date_key(upper(auth_tokens.valid_range)), -1), coalesce(wh_time_key(upper(auth_tokens.valid_range)), -1), upper(auth_tokens.valid_range),
wh_date_key(upper(auth_tokens.valid_range)), wh_time_key(upper(auth_tokens.valid_range)), upper(auth_tokens.valid_range),
auth_tokens.valid_range,
auth_tokens.valid_range,
1
from auth_tokens;
`
// TestRepo creates a repo that can be used for various testing purposes.
func TestRepo(t testing.TB, conn *db.DB) *Repository {
t.Helper()
ctx := context.Background()
require := require.New(t)
rw := db.New(conn)
repo, err := NewRepository(ctx, rw)
require.NoError(err)
return repo
}
// TestGenerateActiveUsers is a test helper that populates the data warehouse
// with active users for the last twelve months.
func TestGenerateActiveUsers(t testing.TB, conn *db.DB) {
t.Helper()
db, err := conn.SqlDB(context.Background())
require.NoError(t, err)
_, err = db.Exec(insertQuery)
require.NoError(t, err)
}
+14 -10
View File
@@ -18,11 +18,13 @@ var (
)
type censusJob struct {
r db.Reader
w db.Writer
lurEnabled bool
agent any
eventCtx context.Context
r db.Reader
w db.Writer
lurEnabled bool
emitEvents bool
sessionsAgent any
activeUsersAgent any
eventCtx context.Context
}
func newCensusJob(ctx context.Context, lurEnabled bool, r db.Reader, w db.Writer) (*censusJob, error) {
@@ -35,11 +37,13 @@ func newCensusJob(ctx context.Context, lurEnabled bool, r db.Reader, w db.Writer
}
return &censusJob{
r: r,
w: w,
lurEnabled: lurEnabled,
agent: nil,
eventCtx: ctx,
r: r,
w: w,
lurEnabled: lurEnabled,
emitEvents: false,
sessionsAgent: nil,
activeUsersAgent: nil,
eventCtx: ctx,
}, nil
}
+12
View File
@@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/commands/authenticate"
"github.com/hashicorp/boundary/internal/cmd/commands/authmethodscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/authtokenscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/billingcmd"
"github.com/hashicorp/boundary/internal/cmd/commands/config"
"github.com/hashicorp/boundary/internal/cmd/commands/connect"
"github.com/hashicorp/boundary/internal/cmd/commands/credentiallibrariescmd"
@@ -243,6 +244,17 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "list",
}),
"billing": func() (cli.Command, error) {
return &billingcmd.Command{
Command: base.NewCommand(ui, opts...),
}, nil
},
"billing monthly-active-users": clientCacheWrapper(
&billingcmd.Command{
Command: base.NewCommand(ui),
Func: "monthly-active-users",
}),
"config": func() (cli.Command, error) {
return &config.Command{
Command: base.NewCommand(ui, opts...),
@@ -0,0 +1,201 @@
// Code generated by "make cli"; DO NOT EDIT.
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billingcmd
import (
"fmt"
"sync"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/billing"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
func initFlags() {
flagsOnce.Do(func() {
extraFlags := extraActionsFlagsMapFunc()
for k, v := range extraFlags {
flagsMap[k] = append(flagsMap[k], v...)
}
})
}
var (
_ cli.Command = (*Command)(nil)
_ cli.CommandAutocomplete = (*Command)(nil)
)
type Command struct {
*base.Command
Func string
plural string
extraCmdVars
}
func (c *Command) AutocompleteArgs() complete.Predictor {
initFlags()
return complete.PredictAnything
}
func (c *Command) AutocompleteFlags() complete.Flags {
initFlags()
return c.Flags().Completions()
}
func (c *Command) Synopsis() string {
if extra := extraSynopsisFunc(c); extra != "" {
return extra
}
synopsisStr := "billing"
return common.SynopsisFunc(c.Func, synopsisStr)
}
func (c *Command) Help() string {
initFlags()
var helpStr string
helpMap := common.HelpMap("billing")
switch c.Func {
default:
helpStr = c.extraHelpFunc(helpMap)
}
// Keep linter from complaining if we don't actually generate code using it
_ = helpMap
return helpStr
}
var flagsMap = map[string][]string{}
func (c *Command) Flags() *base.FlagSets {
if len(flagsMap[c.Func]) == 0 {
return c.FlagSet(base.FlagSetNone)
}
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, "billing", flagsMap, c.Func)
extraFlagsFunc(c, set, f)
return set
}
func (c *Command) Run(args []string) int {
initFlags()
switch c.Func {
case "":
return cli.RunResultHelp
case "create":
return cli.RunResultHelp
case "update":
return cli.RunResultHelp
}
c.plural = "billing"
switch c.Func {
case "list":
c.plural = "billing"
}
f := c.Flags()
if err := f.Parse(args); err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
var opts []billing.Option
if strutil.StrListContains(flagsMap[c.Func], "-id") {
switch c.Func {
}
}
client, err := c.Client()
if c.WrapperCleanupFunc != nil {
defer func() {
if err := c.WrapperCleanupFunc(); err != nil {
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
}
}()
}
if err != nil {
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
return base.CommandCliError
}
billingClient := billing.NewClient(client)
if c.FlagFilter != "" {
opts = append(opts, billing.WithFilter(c.FlagFilter))
}
var version uint32
if ok := extraFlagsHandlingFunc(c, f, &opts); !ok {
return base.CommandUserError
}
var resp *api.Response
resp, err = executeExtraActions(c, resp, err, billingClient, version, opts)
if exitCode := c.checkFuncError(err); exitCode > 0 {
return exitCode
}
output, err := printCustomActionOutput(c)
if err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
if output {
return base.CommandSuccess
}
return base.CommandSuccess
}
func (c *Command) checkFuncError(err error) int {
if err == nil {
return 0
}
if apiErr := api.AsServerError(err); apiErr != nil {
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural))
return base.CommandApiError
}
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
return base.CommandCliError
}
var (
flagsOnce = new(sync.Once)
extraActionsFlagsMapFunc = func() map[string][]string { return nil }
extraSynopsisFunc = func(*Command) string { return "" }
extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {}
extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]billing.Option) bool { return true }
executeExtraActions = func(_ *Command, inResp *api.Response, inErr error, _ *billing.Client, _ uint32, _ []billing.Option) (*api.Response, error) {
return inResp, inErr
}
printCustomActionOutput = func(*Command) (bool, error) { return false, nil }
)
+156
View File
@@ -0,0 +1,156 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billingcmd
import (
"fmt"
"strings"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/billing"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/mitchellh/go-wordwrap"
)
func init() {
extraActionsFlagsMapFunc = extraActionsFlagsMapFuncImpl
extraSynopsisFunc = extraSynopsisFuncImpl
extraFlagsFunc = extraFlagsFuncImpl
extraFlagsHandlingFunc = extraFlagsHandlingFuncImpl
executeExtraActions = executeExtraActionsImpl
printCustomActionOutput = printCustomActionOutputImpl
}
type extraCmdVars struct {
flagStartTime string
flagEndTime string
monthlyActiveUsers *billing.MonthlyActiveUsersResult
}
func extraActionsFlagsMapFuncImpl() map[string][]string {
return map[string][]string{
"monthly-active-users": {"start-time", "end-time"},
}
}
func extraSynopsisFuncImpl(c *Command) string {
switch c.Func {
case "monthly-active-users":
var in string
switch {
case strings.HasPrefix(c.Func, "start-time"):
in = "Get monthly active users, starting from this time (YYYY-MM format)."
case strings.HasPrefix(c.Func, "end-time"):
in = "Get monthly active users, ending at this time (YYYY-MM format)."
}
return wordwrap.WrapString(in, base.TermWidth)
default:
return ""
}
}
func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) {
flagsMap[c.Func] = append(flagsMap[c.Func], "start-time", "end-time")
f.StringVar(&base.StringVar{
Name: "start-time",
Target: &c.flagStartTime,
Usage: "Get monthly active users, starting from this time (YYYY-MM format).",
})
f.StringVar(&base.StringVar{
Name: "end-time",
Target: &c.flagEndTime,
Usage: "Get monthly active users, ending at this time (YYYY-MM format).",
})
}
func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]billing.Option) bool {
switch c.Func {
case "monthly-active-users":
if len(c.flagStartTime) != 0 {
*opts = append(*opts, billing.WithStartTime(c.flagStartTime))
}
if len(c.flagEndTime) != 0 {
*opts = append(*opts, billing.WithEndTime(c.flagEndTime))
}
}
return true
}
func executeExtraActionsImpl(c *Command, origResp *api.Response, origError error, billingClient *billing.Client, _ uint32, opts []billing.Option) (*api.Response, error) {
switch c.Func {
case "monthly-active-users":
var err error
c.monthlyActiveUsers, err = billingClient.MonthlyActiveUsers(c.Context, opts...)
if err != nil {
return nil, err
}
}
return origResp, origError
}
func printCustomActionOutputImpl(c *Command) (bool, error) {
switch c.Func {
case "monthly-active-users":
switch base.Format(c.UI) {
case "table":
items := c.monthlyActiveUsers.GetItems().([]*billing.ActiveUsers)
var ret []string
ret = append(ret, "Billing information:")
ret = append(ret, "")
for i := range items {
ret = append(ret,
fmt.Sprintf(" Count: %d", items[i].Count),
fmt.Sprintf(" Start Time: %s", items[i].StartTime),
fmt.Sprintf(" End Time: %s", items[i].EndTime),
"",
)
}
c.UI.Output(base.WrapForHelpText(ret))
return true, nil
case "json":
if ok := c.PrintJsonItem(c.monthlyActiveUsers.GetResponse()); !ok {
return false, fmt.Errorf("error formatting as JSON")
}
return true, nil
}
}
return false, nil
}
func (c *Command) extraHelpFunc(helpMap map[string]func() string) string {
var helpStr string
switch c.Func {
case "":
helpStr = base.WrapForHelpText([]string{
"Usage: boundary billing [sub command] [options] [args]",
"",
" This command allows for collecting Boundary billing reports. Example:",
"",
" Monthly active users:",
"",
` $ boundary billing monthly-active-users`,
"",
" Please see the billing subcommand help for detailed usage information.",
})
case "monthly-active-users":
helpStr = base.WrapForHelpText([]string{
"Usage: boundary billing monthly-active-users [options]",
"",
" This command allows for collecting active Boundary user reports, by month. Example:",
"",
" Monthly active users between September 2023 and February 2024:",
"",
` $ boundary billing monthly-active-users -start-time="2023-09" -end-time="2024-02"`,
"",
" Please see the billing subcommand help for detailed usage information.",
})
}
return helpStr + c.Flags().Help()
}
+1
View File
@@ -34,6 +34,7 @@ func HelpMap(resType string) map[string]func() string {
resource.AuthToken.String(): "at",
resource.AuthMethod.String(): "am",
resource.Account.String(): "a",
resource.Billing.String(): "b",
resource.Role.String(): "r",
resource.Group.String(): "g",
resource.User.String(): "u",
+12
View File
@@ -17,6 +17,9 @@ type cmdInfo struct {
// Standard actions (with standard parameters) used by this resource
StdActions []string
// HasCustomList indicates if there is a custom list action
HasCustomList bool
// HasExtraCommandVars controls whether to generate an embedded struct with
// extra command variables
HasExtraCommandVars bool
@@ -211,6 +214,15 @@ var inputStructs = map[string][]*cmdInfo{
Container: "Scope",
},
},
"billing": {
{
ResourceType: resource.Billing.String(),
Pkg: "billing",
HasCustomList: true,
HasExtraCommandVars: true,
HasExtraHelpFunc: true,
},
},
"credentialstores": {
{
ResourceType: resource.CredentialStore.String(),
+9 -3
View File
@@ -478,16 +478,19 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
}
var resp *api.Response
{{ if $input.StdActions -}}
var item *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}
{{ if hasAction .StdActions "list" }}
var items []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}
{{ end }}
{{ end }}
{{ range $i, $action := $input.StdActions }}
{{ if ( not ( hasAction $input.SkipClientCallActions $action) ) }}
var {{ $action }}Result *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}{{ camelCase $action }}Result
{{ end }}
{{ end }}
{{ if $input.StdActions }}
switch c.Func {
{{ range $i, $action := $input.StdActions }}
{{ if eq $action "create" }}
@@ -538,8 +541,9 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
{{ end }}
{{ end }}
}
{{ end }}
resp, item, {{ if hasAction .StdActions "list" }}items, {{ end }}err = executeExtra{{ camelCase .SubActionPrefix }}Actions(c, resp, item, {{ if hasAction .StdActions "list" }}items, {{ end }}err, {{ .Pkg }}Client, version, opts)
resp, {{ if $input.StdActions }}item, {{ if hasAction .StdActions "list" }}items, {{ end }}{{ end }}err = executeExtra{{ camelCase .SubActionPrefix }}Actions(c, resp, {{ if $input.StdActions }}item, {{ if hasAction .StdActions "list" }}items, {{ end }}{{ end }}err, {{ .Pkg }}Client, version, opts)
if exitCode := c.checkFuncError(err); exitCode > 0 {
return exitCode
}
@@ -553,6 +557,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
return base.CommandSuccess
}
{{ if $input.StdActions }}
switch c.Func {
{{ range $i, $action := .StdActions }}
{{ if eq $action "delete" }}
@@ -595,6 +600,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
return base.CommandCliError
}
}
{{ end }}
return base.CommandSuccess
}
@@ -619,8 +625,8 @@ var (
extra{{ camelCase .SubActionPrefix }}SynopsisFunc = func(*{{ camelCase .SubActionPrefix }}Command) string { return "" }
extra{{ camelCase .SubActionPrefix }}FlagsFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *base.FlagSet) {}
extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *[]{{ .Pkg }}.Option) bool { return true }
executeExtra{{ camelCase .SubActionPrefix }}Actions = func(_ *{{ camelCase .SubActionPrefix }}Command, inResp *api.Response, inItem *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}inItems []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}inErr error, _ *{{ .Pkg }}.Client, _ uint32, _ []{{ .Pkg }}.Option) (*api.Response, *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}[]*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}error) {
return inResp, inItem, {{ if hasAction .StdActions "list" }}inItems, {{ end }}inErr
executeExtra{{ camelCase .SubActionPrefix }}Actions = func(_ *{{ camelCase .SubActionPrefix }}Command, inResp *api.Response, {{ if $input.StdActions }}inItem *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}inItems []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}{{ end }}inErr error, _ *{{ .Pkg }}.Client, _ uint32, _ []{{ .Pkg }}.Option) (*api.Response, {{ if $input.StdActions }}*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}[]*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}{{ end }}error) {
return inResp, {{ if $input.StdActions }}inItem, {{ if hasAction .StdActions "list" }}inItems, {{ end }}{{ end }}inErr
}
printCustom{{ camelCase .SubActionPrefix }}ActionOutput = func(*{{ camelCase .SubActionPrefix }}Command) (bool, error) { return false, nil }
)
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/credential"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
@@ -39,6 +40,7 @@ type (
ConnectionRepoFactory func() (*session.ConnectionRepository, error)
WorkerAuthRepoStorageFactory func() (*server.WorkerAuthRepositoryStorage, error)
PluginStorageBucketRepoFactory func() (*pluginstorage.Repository, error)
BillingRepoFactory func() (*billing.Repository, error)
)
// Downstreamers provides at least a minimum interface that must be met by a
+5
View File
@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/census"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/config"
@@ -148,6 +149,7 @@ type Controller struct {
PluginRepoFn common.PluginRepoFactory
TargetRepoFn target.RepositoryFactory
WorkerAuthRepoStorageFn common.WorkerAuthRepoStorageFactory
BillingRepoFn common.BillingRepoFactory
scheduler *scheduler.Scheduler
@@ -448,6 +450,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
c.WorkerAuthRepoStorageFn = func() (*server.WorkerAuthRepositoryStorage, error) {
return server.NewRepositoryStorage(ctx, dbase, dbase, c.kms)
}
c.BillingRepoFn = func() (*billing.Repository, error) {
return billing.NewRepository(ctx, dbase)
}
// Check that credentials are available at startup, to avoid some harmless
// but nasty-looking errors
+11
View File
@@ -24,6 +24,7 @@ import (
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
@@ -349,6 +350,13 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
opsservices.RegisterHealthServiceServer(s, hs)
c.HealthService = hs
}
if _, ok := currentServices[services.BillingService_ServiceDesc.ServiceName]; !ok {
bs, err := billing.NewService(c.baseContext, c.BillingRepoFn)
if err != nil {
return fmt.Errorf("failed to create billing handler service: %w", err)
}
services.RegisterBillingServiceServer(s, bs)
}
return nil
}
@@ -419,6 +427,9 @@ func registerGrpcGatewayEndpoints(ctx context.Context, gwMux *runtime.ServeMux,
if err := services.RegisterPolicyServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
return fmt.Errorf("failed to register policy handler: %w", err)
}
if err := services.RegisterBillingServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
return fmt.Errorf("failed to register billing service handler: %w", err)
}
return nil
}
@@ -137,6 +137,7 @@ func TestHandleImplementedPaths(t *testing.T) {
"v1/targets/someid",
"v1/users",
"v1/users/someid",
"v1/billing:monthly-active-users",
},
"POST": {
// Creation end points
@@ -0,0 +1,125 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
// IdActions contains the set of actions that can be performed on
// individual resources
IdActions = action.NewActionSet()
// CollectionActions contains the set of actions that can be performed on
// this collection
CollectionActions = action.NewActionSet(
action.MonthlyActiveUsers,
)
)
func init() {
// TODO: refactor to remove IdActions and CollectionActions package variables
action.RegisterResource(resource.Billing, IdActions, CollectionActions)
}
type Service struct {
pbs.UnsafeBillingServiceServer
repoFn common.BillingRepoFactory
}
var _ pbs.BillingServiceServer = (*Service)(nil)
// NewService returns a billing service which handles billing related requests to boundary.
func NewService(
ctx context.Context,
repoFn common.BillingRepoFactory,
) (Service, error) {
const op = "billing.NewService"
if repoFn == nil {
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing billing repository")
}
return Service{
repoFn: repoFn,
}, nil
}
func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveUsersRequest) (*pbs.MonthlyActiveUsersResponse, error) {
const op = "billing.(Service).MonthlyActiveUsers"
authResults := s.authResult(ctx, action.MonthlyActiveUsers)
if authResults.Error != nil {
return nil, errors.Wrap(ctx, authResults.Error, op)
}
// Get the billing information
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var startTime, endTime *time.Time
if req.GetStartTime() != "" {
st, err := time.Parse("2006-01", req.GetStartTime())
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "start time is in an invalid format")
}
startTime = &st
}
if req.GetEndTime() != "" {
et, err := time.Parse("2006-01", req.GetEndTime())
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "end time is in an invalid format")
}
endTime = &et
}
if startTime != nil && endTime != nil && !endTime.After(*startTime) {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "start time is not before end time")
}
months, err := repo.MonthlyActiveUsers(
ctx,
billing.WithStartTime(startTime),
billing.WithEndTime(endTime),
)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error())
}
var activeUsers []*pb.ActiveUsers
for _, month := range months {
maud := &pb.ActiveUsers{
StartTime: timestamppb.New(month.StartTime),
EndTime: timestamppb.New(month.EndTime),
Count: month.ActiveUsersCount,
}
activeUsers = append(activeUsers, maud)
}
return &pbs.MonthlyActiveUsersResponse{Items: activeUsers}, nil
}
func (s Service) authResult(ctx context.Context, a action.Type) auth.VerifyResults {
opts := []auth.Option{
auth.WithType(resource.Billing),
auth.WithAction(a),
auth.WithScopeId(scope.Global.String()),
}
return auth.Verify(ctx, opts...)
}
@@ -0,0 +1,169 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing_test
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
billingservice "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_MonthlyActiveUsers(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
repoFn := func() (*billing.Repository, error) {
return billing.TestRepo(t, conn), nil
}
billing.TestGenerateActiveUsers(t, conn)
wrap := db.TestWrapper(t)
iamRepoFn := func() (*iam.Repository, error) {
return iam.TestRepo(t, conn, wrap), nil
}
today := time.Now().UTC()
threeMonthsAgo := time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
oneMonthAgo := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
badFormat := time.Date(today.Year(), today.Month(), 15, 0, 0, 0, 0, time.UTC).String()
cases := []struct {
name string
req *pbs.MonthlyActiveUsersRequest
res *pbs.MonthlyActiveUsersResponse
errContains string
}{
{
name: "Valid no options, current and previous months",
req: &pbs.MonthlyActiveUsersRequest{},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 0,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Valid start time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 0,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-3, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Valid start and end time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: oneMonthAgo},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-3, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Invalid end time without start time",
req: &pbs.MonthlyActiveUsersRequest{EndTime: oneMonthAgo},
errContains: "end time set without start time",
},
{
name: "Invalid end time before start time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: oneMonthAgo, EndTime: threeMonthsAgo},
errContains: "start time is not before end time",
},
{
name: "Invalid start time equals end time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: threeMonthsAgo},
errContains: "start time is not before end time",
},
{
name: "Invalid start time format",
req: &pbs.MonthlyActiveUsersRequest{StartTime: badFormat},
errContains: "start time is in an invalid format",
},
{
name: "Invalid end time format",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: badFormat},
errContains: "end time is in an invalid format",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
b, err := billingservice.NewService(ctx, repoFn)
require.NoError(t, err, "Couldn't create new billing service.")
got, gErr := b.MonthlyActiveUsers(auth.DisabledAuthTestContext(iamRepoFn, scope.Global.String(), auth.WithUserId(globals.AnyAuthenticatedUserId)), tc.req)
if tc.errContains != "" {
require.ErrorContains(t, gErr, tc.errContains)
require.Nil(t, got)
return
} else {
require.NoError(t, gErr)
}
assert.Empty(t,
cmp.Diff(
got,
tc.res,
protocmp.Transform(),
protocmp.SortRepeatedFields(got),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
))
})
}
}
@@ -63,7 +63,7 @@ func Test_newRateLimiterConfig(t *testing.T) {
ratelimit.DefaultLimiterMaxQuotas(),
false,
&rateLimiterConfig{
maxSize: 322161,
maxSize: 324162,
configs: nil,
disabled: false,
limits: defaultLimits,
@@ -4075,7 +4075,7 @@
]
}
},
"max_size": 322161,
"max_size": 324162,
"msg": "controller api rate limiter"
},
"op": "controller.(rateLimiterConfig).writeSysEvent",
@@ -4075,7 +4075,7 @@
]
}
},
"max_size": 322161,
"max_size": 324162,
"msg": "controller api rate limiter"
},
"op": "controller.(rateLimiterConfig).writeSysEvent",
@@ -97,6 +97,7 @@ begin;
;
-- replaces function from 14/01_wh_user_dimension_oidc.up.sql
-- replaced in 82/02_wh_upsert_user_refact.up.sql
drop function wh_upsert_user;
create function wh_upsert_user(p_user_id wt_user_id, p_auth_token_id wt_public_id) returns wh_dim_key
as $$
@@ -216,7 +216,8 @@ begin;
create trigger wh_update_session_connection_accumulating_fact after insert on session_host_set_host
for each row execute procedure wh_upsert_host();
-- replaced function in 16/04_wh_credential_dimension.up.sql
-- Replaces function from 16/04_wh_credential_dimension.up.sql
-- Replaced in 82/02_wh_upsert_user_refact.up.sql
create or replace function wh_insert_session() returns trigger
as $$
declare
@@ -136,6 +136,7 @@ begin;
comment on table credential_vault_ssh_cert_library is
'credential_vault_ssh_cert_library a credential library that issues credentials from a vault ssh secret backend.';
-- Replaced in 82/07_vault_ssh_cert_default.up.sql
create function default_ssh_certificate_credential_type() returns trigger
as $$
begin
@@ -12,6 +12,7 @@ begin;
-- This index ensures that there will only ever be one row in the table.
-- See: https://www.postgresql.org/docs/current/indexes-expressional.html
-- Dropped in 82/09_census_add_metric.up.sql
create unique index census_last_uploaded_one_row
on census_last_uploaded((last_uploaded_at is not null));
@@ -0,0 +1,101 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- wh_user_id is similar to wt_user_id but for the data warehouse.
-- Unlike wt_user_id, this allows for nulls.
create domain wh_user_id as text
check(
length(trim(value)) > 10 or value in ('u_anon', 'u_auth', 'u_recovery')
);
comment on domain wh_user_id is
'"u_anon", "u_auth", "u_recovery" or random ID generated with github.com/hashicorp/go-secure-stdlib/base62';
create table wh_auth_token_accumulating_fact (
auth_token_id wt_public_id primary key,
user_id wh_user_id not null,
user_key wh_dim_key not null
references wh_user_dimension (key)
on delete restrict
on update cascade,
-- date and time foreign keys
auth_token_issued_date_key integer not null
references wh_date_dimension (key)
on delete restrict
on update cascade,
auth_token_issued_time_key integer not null
references wh_time_of_day_dimension (key)
on delete restrict
on update cascade,
auth_token_issued_time wh_timestamp,
auth_token_deleted_date_key integer default -1 not null
references wh_date_dimension (key)
on delete restrict
on update cascade,
auth_token_deleted_time_key integer default -1 not null
references wh_time_of_day_dimension (key)
on delete restrict
on update cascade,
auth_token_deleted_time wh_timestamp default 'infinity'::timestamptz,
auth_token_approximate_last_access_date_key integer default -1 not null
references wh_date_dimension (key)
on delete restrict
on update cascade,
auth_token_approximate_last_access_time_key integer default -1 not null
references wh_time_of_day_dimension (key)
on delete restrict
on update cascade,
auth_token_approximate_last_access_time wh_timestamp,
auth_token_approximate_active_time_range tstzrange not null default tstzrange(current_timestamp, current_timestamp, '[]'),
auth_token_valid_time_range tstzrange not null default tstzrange(current_timestamp, 'infinity'::timestamptz, '[]'),
-- the auth_token_count must always be 1
-- this is a common pattern in data warehouse models
-- See The Data Warehouse Toolkit, Third Edition
-- by Ralph Kimball and Margy Ross for more information
auth_token_count smallint default 1 not null
constraint auth_token_count_must_be_1
check(auth_token_count = 1),
constraint last_accessed_time_lte_deleted_time
check(auth_token_approximate_last_access_time <= auth_token_deleted_time),
constraint active_time_lower_eq_issued_time
check(lower(auth_token_approximate_active_time_range) = auth_token_issued_time),
constraint active_time_upper_eq_last_accessed_time
check(upper(auth_token_approximate_active_time_range) = auth_token_approximate_last_access_time),
constraint valid_time_lower_eq_issued
check(lower(auth_token_valid_time_range) = auth_token_issued_time),
constraint valid_time_upper_eq_deleted
check(upper(auth_token_valid_time_range) = auth_token_deleted_time),
constraint active_time_contained_by_valid_time
check(auth_token_approximate_active_time_range <@ auth_token_valid_time_range)
);
comment on table wh_auth_token_accumulating_fact is
'The Wh Auth Token Accumulating Fact table is an accumulating fact table. '
'The grain of the fact table is one row per auth token.';
create function wh_insert_auth_token() returns trigger
as $$
begin
select user_id
into new.user_id
from wh_user_dimension
where key = new.user_key;
return new;
end;
$$ language plpgsql;
create trigger wh_insert_auth_token before insert on wh_auth_token_accumulating_fact
for each row execute function wh_insert_auth_token();
create trigger immutable_columns before update on wh_auth_token_accumulating_fact
for each row execute procedure immutable_columns('user_id', 'user_key', 'auth_token_issued_time', 'auth_token_issued_time_key', 'auth_token_issued_date_key');
commit;
@@ -0,0 +1,111 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
drop trigger wh_insert_session on session;
drop function wh_insert_session;
drop function wh_upsert_user;
-- Replaces function from 15/01_wh_rename_key_columns.up.sql
create function wh_upsert_user(p_auth_token_id wt_public_id) returns wh_dim_key
as $$
declare
src whx_user_dimension_target%rowtype;
target whx_user_dimension_target%rowtype;
new_row wh_user_dimension%rowtype;
acct_id wt_public_id;
begin
select auth_account_id into strict acct_id
from auth_token
where public_id = p_auth_token_id;
select * into target
from whx_user_dimension_target as t
where t.auth_account_id = acct_id;
select target.key, t.* into src
from whx_user_dimension_source as t
where t.auth_account_id = acct_id;
if src is distinct from target then
-- expire the current row
update wh_user_dimension
set current_row_indicator = 'Expired',
row_expiration_time = current_timestamp
where auth_account_id = acct_id
and current_row_indicator = 'Current';
-- insert a new row
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_account_external_id, auth_account_full_name, auth_account_email,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
auth_method_external_id,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator, row_effective_time, row_expiration_time
)
select user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_account_external_id, auth_account_full_name, auth_account_email,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
auth_method_external_id,
user_organization_id, user_organization_name, user_organization_description,
'Current', current_timestamp, 'infinity'::timestamptz
from whx_user_dimension_source
where auth_account_id = acct_id
returning * into new_row;
return new_row.key;
end if;
return target.key;
end;
$$ language plpgsql;
comment on function wh_upsert_user is
'wh_upsert_user a function insert or updates the wh_user_dimension table'
'for the user that corresponds to the provided auth_token_id.';
-- Replaces function from 60/03_wh_sessions.up.sql
create function wh_insert_session() returns trigger
as $$
declare
new_row wh_session_accumulating_fact%rowtype;
begin
with
pending_timestamp (date_dim_key, time_dim_key, ts) as (
select wh_date_key(start_time), wh_time_key(start_time), start_time
from session_state
where session_id = new.public_id
and state = 'pending'
)
insert into wh_session_accumulating_fact (
session_id,
auth_token_id,
host_key,
user_key,
credential_group_key,
session_pending_date_key,
session_pending_time_key,
session_pending_time
)
select new.public_id,
new.auth_token_id,
'no host source', -- will be updated by wh_upsert_host
wh_upsert_user(new.auth_token_id),
'no credentials', -- will be updated by wh_upsert_credential_group
pending_timestamp.date_dim_key,
pending_timestamp.time_dim_key,
pending_timestamp.ts
from pending_timestamp
returning * into strict new_row;
return null;
end;
$$ language plpgsql;
create trigger wh_insert_session after insert on session
for each row execute procedure wh_insert_session();
commit;
@@ -0,0 +1,19 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create function auth_token_before_issued() returns trigger
as $$
begin
if new.status = 'token issued' then
new.approximate_last_access_time = now();
end if;
return new;
end;
$$ language plpgsql;
create trigger auth_token_before_inserted before insert on auth_token
for each row execute procedure auth_token_before_issued();
create trigger auth_token_before_updated before update on auth_token
for each row execute procedure auth_token_before_issued();
commit;
@@ -0,0 +1,51 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create function wh_auth_token_issued(p_auth_token_id wt_public_id, issued_ts wt_timestamp, last_accessed_ts wt_timestamp) returns void
as $$
declare
new_row wh_auth_token_accumulating_fact%rowtype;
begin
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range
)
select p_auth_token_id,
wh_upsert_user(p_auth_token_id),
wh_date_key(issued_ts),
wh_time_key(issued_ts),
issued_ts,
wh_date_key(last_accessed_ts),
wh_time_key(last_accessed_ts),
last_accessed_ts,
tstzrange(issued_ts, last_accessed_ts, '[]'),
tstzrange(issued_ts, 'infinity'::wt_timestamp, '[]')
returning * into strict new_row;
return;
end;
$$ language plpgsql;
comment on function wh_auth_token_issued is
'wh_auth_token_issued is a function called when an auth token is issued to insert a fact into the auth token accumulating fact table.';
create function auth_token_inserted() returns trigger
as $$
begin
if new.status = 'token issued' then
perform wh_auth_token_issued(new.public_id, new.update_time, new.approximate_last_access_time);
end if;
return null;
end;
$$ language plpgsql;
create trigger auth_token_inserted after insert on auth_token
for each row execute procedure auth_token_inserted();
commit;
@@ -0,0 +1,38 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create function wh_auth_token_accessed(p_auth_token_id wt_public_id, last_accessed_ts wt_timestamp) returns void
as $$
begin
update wh_auth_token_accumulating_fact
set auth_token_approximate_last_access_date_key = wh_date_key(last_accessed_ts),
auth_token_approximate_last_access_time_key = wh_time_key(last_accessed_ts),
auth_token_approximate_last_access_time = last_accessed_ts,
auth_token_approximate_active_time_range = tstzrange(lower(auth_token_approximate_active_time_range), last_accessed_ts, '[]')
where auth_token_id = p_auth_token_id;
return;
end;
$$ language plpgsql;
comment on function wh_auth_token_accessed is
'wh_auth_token_accessed is a function that updates the wh_auth_token_accumulating_fact'
'when an auth_token is accessed.';
create function auth_token_updated() returns trigger
as $$
begin
case
when new.status = 'token issued' and new.status is distinct from old.status then
perform wh_auth_token_issued(new.public_id, new.update_time, new.approximate_last_access_time);
when new.approximate_last_access_time is distinct from old.approximate_last_access_time then
perform wh_auth_token_accessed(new.public_id, new.approximate_last_access_time);
else
return null;
end case;
return null;
end;
$$ language plpgsql;
create trigger auth_token_updated after update on auth_token
for each row execute procedure auth_token_updated();
commit;
@@ -0,0 +1,33 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create function wh_auth_token_deleted(p_auth_token_id wt_public_id) returns void
as $$
begin
update wh_auth_token_accumulating_fact
set auth_token_deleted_date_key = wh_date_key(now()),
auth_token_deleted_time_key = wh_time_key(now()),
auth_token_deleted_time = now(),
auth_token_valid_time_range = tstzrange(lower(auth_token_valid_time_range), now(), '[]')
where auth_token_id = p_auth_token_id;
return;
end;
$$ language plpgsql;
comment on function wh_auth_token_deleted is
'wh_auth_token_deleted is a function that updates the wh_auth_token_accumulating_fact'
'when a previously issued auth_token is deleted.';
create function auth_token_deleted() returns trigger
as $$
begin
if old.status = 'token issued' then
perform wh_auth_token_deleted(old.public_id);
end if;
return null;
end;
$$ language plpgsql;
create trigger auth_token_deleted after delete on auth_token
for each row execute procedure auth_token_deleted();
commit;
@@ -0,0 +1,23 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
drop trigger default_ssh_certificate_credential_type on credential_vault_ssh_cert_library;
drop function default_ssh_certificate_credential_type;
-- Replaces trigger in 63/01_credential_vault_ssh_cert_library.up.sql
create function default_ssh_certificate_credential_type() returns trigger
as $$
begin
if new.credential_type is distinct from 'ssh_certificate' then
new.credential_type = 'ssh_certificate';
end if;
return new;
end;
$$ language plpgsql;
comment on function default_ssh_certificate_credential_type is
'default_ssh_certificate_credential_type ensures the credential_type is set to ssh_certificate';
create trigger default_ssh_certificate_credential_type before insert on credential_vault_ssh_cert_library
for each row execute procedure default_ssh_certificate_credential_type();
commit;
@@ -0,0 +1,121 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create view hcp_billing_monthly_active_users_last_2_months as
with
monthly_range (month) as (
select date_trunc('month', time) as month
from generate_series(
date_trunc('month', now() - interval '1 month'),
now(),
'1 month'::interval
) as time
),
final (start_time, end_time, active_users_count) as (
select monthly_range.month, -- start
case when monthly_range.month = date_trunc('month', now())
then date_trunc('hour', now())
else monthly_range.month + interval '1 month'
end,
count(distinct(user_id))
from monthly_range
left join wh_auth_token_accumulating_fact as wh_ataf
on wh_ataf.auth_token_approximate_active_time_range && tstzrange(monthly_range.month, monthly_range.month + interval '1 month', '[)')
group by monthly_range.month
)
select start_time, end_time, active_users_count
from final
order by start_time desc;
comment on view hcp_billing_monthly_active_users_last_2_months is
'hcp_billing_monthly_active_users_last_2_months is a view that contains '
'the count of active users for the current month and the previous month. '
'The current month is a sum from the beginning of the current month '
'until the start of the current hour (exclusive).';
create view hcp_billing_monthly_active_users_all as
with
monthly_range (month) as (
select date_trunc('month', time) as month
from generate_series(
date_trunc('month', (select min(auth_token_issued_time)
from wh_auth_token_accumulating_fact)),
now(),
'1 month'::interval
) as time
),
final (start_time, end_time, active_users_count) as (
select monthly_range.month, -- start
case when monthly_range.month = date_trunc('month', now())
then date_trunc('hour', now())
else monthly_range.month + interval '1 month'
end,
count(distinct(user_id))
from monthly_range
left join wh_auth_token_accumulating_fact as wh_ataf
on wh_ataf.auth_token_approximate_active_time_range && tstzrange(monthly_range.month, monthly_range.month + interval '1 month', '[)')
group by monthly_range.month
)
select start_time, end_time, active_users_count
from final
order by start_time desc;
comment on view hcp_billing_monthly_active_users_all is
'hcp_billing_monthly_active_users_all is a view that contains '
'the count of active users for the all months.'
'The current month is a sum from the beginning of the current month '
'until the start of the current hour (exclusive).';
create function hcp_billing_monthly_active_users_last_2_months()
returns table(start_time timestamp with time zone, end_time timestamp with time zone, active_user_count bigint)
as $$
select *
from hcp_billing_monthly_active_users_last_2_months;
$$ language sql
immutable
parallel safe -- all of the functions called are parallel safe
strict -- means the function returns null on null input
set timezone to 'utc';
comment on function hcp_billing_monthly_active_users_last_2_months is
'hcp_billing_monthly_active_users_last_2_months is a function that contains '
'the count of active users for the current month and the previous month. '
'The current month is a sum from the beginning of the current month '
'until the start of the current hour (exclusive).'
'All timestamps returned are in UTC.';
create function hcp_billing_monthly_active_users_all(p_start_time timestamptz default null,
p_end_time timestamptz default null)
returns setof hcp_billing_monthly_active_users_all
as $$
begin
case
when p_start_time is not null and p_end_time is not null then
return query select *
from hcp_billing_monthly_active_users_all
where start_time >= p_start_time
and end_time <= p_end_time;
when p_start_time is not null then
return query select *
from hcp_billing_monthly_active_users_all
where start_time >= p_start_time;
when p_end_time is not null then
return query select *
from hcp_billing_monthly_active_users_all
where end_time <= p_end_time;
else
return query select *
from hcp_billing_monthly_active_users_all;
end case;
return;
end;
$$ language plpgsql
immutable
parallel safe -- all of the functions called are parallel safe
set timezone to 'utc';
comment on function hcp_billing_monthly_active_users_all is
'hcp_billing_monthly_active_users_all is a function that contains '
'the count of active users for the all months.'
'The current month is a sum from the beginning of the current month '
'until the start of the current hour (exclusive).'
'It can be provided with a start time and end time to restrict the results.'
'All timestamps returned are in UTC.';
commit;
@@ -0,0 +1,47 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- Create enum table for valid cenus metrics.
create table census_metric_enm (
name text primary key
constraint only_predefined_census_metrics_allowed
check (
name in (
'sessions',
'active_users'
)
)
);
comment on table census_metric_enm is
'census_metric_enm is an enumeration table for census metric types.';
insert into census_metric_enm
(name)
values ('sessions'),
('active_users');
-- Drop constraint and index from 72/01_census.up.sql
drop index census_last_uploaded_one_row;
alter table census_last_uploaded drop constraint census_last_uploaded_pkey;
-- Add new column and populate the default value.
alter table census_last_uploaded
add column metric text
default 'sessions'
not null
primary key
constraint census_metric_enm_fkey
references census_metric_enm (name)
on delete restrict
on update restrict;
alter table census_last_uploaded alter column metric drop default;
comment on table census_last_uploaded is
'census_last_uploaded is a table which contains the timestamp '
'of the last time census data was uploaded, per census metric.';
-- Add new row for active_users.
insert into census_last_uploaded
(last_uploaded_at, metric)
values ('-infinity'::timestamptz, 'active_users');
commit;
+6 -2
View File
@@ -12,7 +12,6 @@ ifneq ($(strip $(SQL_TEST_DB_PORT)),)
DOCKER_ARGS += -p $(SQL_TEST_DB_PORT):5432
endif
# Pass through options to pg_prove
# See: https://pgtap.org/pg_prove.html
PROVE_OPTS ?=
@@ -47,6 +46,11 @@ PG_TAP_DOCKER_IMAGE := $(PG_TAP_DOCKER_IMAGE_BASE):$(PG_TAP_DOCKER_TAG)
SQL_TEST_CONTAINER_NAME ?= boundary-sql-tests
log_cmd = true
DB_LOGS ?= 0
ifeq ($(DB_LOGS), 1)
log_cmd = docker logs $(SQL_TEST_CONTAINER_NAME)
endif
# re-write paths for docker
dockerized_tests = $(patsubst tests/%,/test/%,$(TESTS))
@@ -74,7 +78,7 @@ test:
-e TESTS="$(PROVE_OPTS) $(dockerized_tests)" \
-v "$(CWD)/tests":/test \
$(PG_TAP_DOCKER_IMAGE); \
(ret=$$?; docker stop $(SQL_TEST_CONTAINER_NAME) &>/dev/null && docker logs $(SQL_TEST_CONTAINER_NAME) && docker rm -v $(SQL_TEST_CONTAINER_NAME) &>/dev/null && exit $$ret)
(ret=$$?; docker stop $(SQL_TEST_CONTAINER_NAME) > /dev/null 2>&1 && $(log_cmd) && docker rm -f -v $(SQL_TEST_CONTAINER_NAME) > /dev/null 2>&1 && exit $$ret)
database-up:
@echo Using $(POSTGRES_DOCKER_IMAGE)
@@ -199,13 +199,13 @@ begin;
('p____gcolors', 'hc__st_____g', 'Green Color Static Catalog');
insert into auth_token
(key_id, auth_account_id, public_id, token)
(key_id, auth_account_id, public_id, token, expiration_time, status)
values
('kdkv__colors', 'apa____clare', 'tok____clare', 'tok____clare'::bytea),
('kdkv__colors', 'apa____cindy', 'tok____cindy', 'tok____cindy'::bytea),
('kdkv__colors', 'apa____ciara', 'tok____ciara', 'tok____ciara'::bytea),
('kdkv__colors', 'apa____carly', 'tok____carly', 'tok____carly'::bytea),
('kdkv__colors', 'apa_____cora', 'tok_____cora', 'tok_____cora'::bytea);
('kdkv__colors', 'apa____clare', 'tok____clare', 'tok____clare'::bytea, now() + interval '15 days', 'token issued'),
('kdkv__colors', 'apa____cindy', 'tok____cindy', 'tok____cindy'::bytea, now() + interval '15 days', 'token issued'),
('kdkv__colors', 'apa____ciara', 'tok____ciara', 'tok____ciara'::bytea, now() + interval '15 days', 'auth token pending'),
('kdkv__colors', 'apa____carly', 'tok____carly', 'tok____carly'::bytea, now() + interval '15 days', 'token issued'),
('kdkv__colors', 'apa_____cora', 'tok_____cora', 'tok_____cora'::bytea, now() + interval '15 days', 'auth token pending');
insert into static_host
(catalog_id, public_id, address)
@@ -133,7 +133,7 @@ begin;
(key_id, table_name, total_count)
values
('kdkv___widget', 'auth_token', 100);
end;
$$ language plpgsql;
@@ -172,13 +172,13 @@ begin;
update auth_account set iam_user_id = 'u_____wilson' where public_id = 'apa___wilson';
insert into auth_token
(key_id, auth_account_id, public_id, token)
(key_id, auth_account_id, public_id, token, expiration_time, status)
values
('kdkv___widget', 'apa___walter', 'tok___walter', 'tok___walter'::bytea),
('kdkv___widget', 'apa1__walter', 'tok1__walter', 'tok1__walter'::bytea),
('kdkv___widget', 'apa___warren', 'tok___warren', 'tok___warren'::bytea),
('kdkv___widget', 'apa___waylon', 'tok___waylon', 'tok___waylon'::bytea),
('kdkv___widget', 'apa___wilson', 'tok___wilson', 'tok___wilson'::bytea);
('kdkv___widget', 'apa___walter', 'tok___walter', 'tok___walter'::bytea, now() + interval '15 days', 'token issued'),
('kdkv___widget', 'apa1__walter', 'tok1__walter', 'tok1__walter'::bytea, now() + interval '15 days', 'token issued'),
('kdkv___widget', 'apa___warren', 'tok___warren', 'tok___warren'::bytea, now() + interval '15 days', 'token issued'),
('kdkv___widget', 'apa___waylon', 'tok___waylon', 'tok___waylon'::bytea, now() + interval '15 days', 'token issued'),
('kdkv___widget', 'apa___wilson', 'tok___wilson', 'tok___wilson'::bytea, now() + interval '15 days', 'auth token pending');
insert into auth_oidc_method
(scope_id, public_id, client_id, name, state, key_id, issuer)
@@ -206,7 +206,7 @@ begin;
('o_____widget', 'alm___widget', 'Widget LDAP', 'active-private');
insert into auth_ldap_url
(ldap_method_id, url, connection_priority)
values
values
('alm___widget', 'ldaps://ldap1', 1);
insert into auth_ldap_account
@@ -3,28 +3,55 @@
begin;
select plan(6);
select plan(7);
select has_table('census_last_uploaded');
select is(count(*), 1::bigint, 'census_last_uploaded should have only 1 row') from census_last_uploaded;
select ok(not isfinite(last_uploaded_at)) from census_last_uploaded;
select is(count(*), 2::bigint, 'census_last_uploaded should have only 2 row') from census_last_uploaded;
select results_eq(
$$
select last_uploaded_at::timestamptz, metric
from census_last_uploaded
$$,
$$
values ('-infinity'::timestamptz, 'sessions'),
('-infinity'::timestamptz, 'active_users')
$$);
prepare insert_row as
insert into census_last_uploaded
(last_uploaded_at)
values
(now());
(last_uploaded_at, metric)
values (now(), 'sessions');
select throws_ok('insert_row', '23505',
'duplicate key value violates unique constraint "census_last_uploaded_one_row"',
'insert into census_last_uploaded should fail');
select throws_ok('insert_row',
'23505',
'duplicate key value violates unique constraint "census_last_uploaded_pkey"',
'insert into census_last_uploaded should fail');
prepare insert_row_invalid_metric as
insert into census_last_uploaded
(last_uploaded_at, metric)
values (now(), 'foo');
select throws_ok('insert_row_invalid_metric',
'23503',
'insert or update on table "census_last_uploaded" violates foreign key constraint "census_metric_enm_fkey"',
'insert into census_last_uploaded should fail');
prepare update_census_last_uploaded as
update census_last_uploaded
set last_uploaded_at = now();
select lives_ok('update_census_last_uploaded');
select ok(isfinite(last_uploaded_at)) from census_last_uploaded;
select results_eq(
$$
select last_uploaded_at::timestamptz, metric
from census_last_uploaded
$$,
$$
values (now(), 'sessions'),
(now(), 'active_users')
$$);
select * from finish();
@@ -42,22 +42,22 @@ begin;
select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8';
select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8';
prepare insert_credential_vault_library_ssh_private_key_mapping_override as
prepare insert_cvl_ssh_private_key_mapping_override as
insert into credential_vault_library_ssh_private_key_mapping_override
(library_id, username_attribute, private_key_attribute)
values
('vl______wvl8', 'my_username', 'my_private_key');
select lives_ok('insert_credential_vault_library_ssh_private_key_mapping_override');
select lives_ok('insert_cvl_ssh_private_key_mapping_override');
select is(count(*), 1::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8';
select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8';
-- validate the delete triggers
prepare delete_credential_vault_library_ssh_private_key_mapping_override as
prepare delete_cvl_ssh_private_key_mapping_override as
delete
from credential_vault_library_ssh_private_key_mapping_override
where library_id = 'vl______wvl8';
select lives_ok('delete_credential_vault_library_ssh_private_key_mapping_override');
select lives_ok('delete_cvl_ssh_private_key_mapping_override');
select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8';
select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8';
@@ -42,22 +42,22 @@ begin;
select is(count(*), 0::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3';
select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3';
prepare insert_credential_vault_library_username_password_mapping_override as
prepare insert_cvl_username_password_mapping_override as
insert into credential_vault_library_username_password_mapping_override
(library_id, username_attribute, password_attribute)
values
('vl______wvl3', 'my_username', 'my_password');
select lives_ok('insert_credential_vault_library_username_password_mapping_override');
select lives_ok('insert_cvl_username_password_mapping_override');
select is(count(*), 1::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3';
select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3';
-- validate the delete triggers
prepare delete_credential_vault_library_username_password_mapping_override as
prepare delete_cvl_username_password_mapping_override as
delete
from credential_vault_library_username_password_mapping_override
where library_id = 'vl______wvl3';
select lives_ok('delete_credential_vault_library_username_password_mapping_override');
select lives_ok('delete_cvl_username_password_mapping_override');
select is(count(*), 0::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3';
select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3';
@@ -0,0 +1,37 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(3);
create table test_wh_user_id (
id wh_user_id
);
prepare too_short as
insert into test_wh_user_id (id)
values ('short');
select throws_ok(
'too_short',
'23514',
'value for domain wh_user_id violates check constraint "wh_user_id_check"'
);
prepare too_short_trim as
insert into test_wh_user_id (id)
values (' short ');
select throws_ok(
'too_short_trim',
'23514',
'value for domain wh_user_id violates check constraint "wh_user_id_check"'
);
prepare valid as
insert into test_wh_user_id (id)
values ('u_123456789'),
('u_anon'),
('u_auth'),
('u_recovery');
select lives_ok('valid');
select * from finish();
rollback;
@@ -0,0 +1,470 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(39);
select lives_ok('truncate wh_auth_token_accumulating_fact,
wh_user_dimension,
wh_session_accumulating_fact,
wh_session_connection_accumulating_fact;',
'Truncate tables in preparation for testing');
-- validate the warehouse fact tables are empty
select is(count(*), 0::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 0::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
-- Create a user dimension for corresponding auth token's user.
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email
) values (
'u_____user1', 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
);
-- validate view returns 0 active users for last two months
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 0::bigint)
$$);
-- Now insert some edge cases to make sure we count them correctly
-- active_time_range spans start of a month
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
select tstzrange(lower(previous) - interval '5 minutes', lower(previous) + interval '5 minutes', '[]'),
tstzrange(lower(previous) - interval '5 minutes', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range within start/end of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
select tstzrange(lower(previous) + interval '5 minutes', lower(previous) + interval '10 minutes', '[]'),
tstzrange(lower(previous) + interval '5 minutes', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range spances end of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
select tstzrange(upper(previous) - interval '5 minutes', upper(previous) + interval '5 minutes', '[]'),
tstzrange(upper(previous) - interval '5 minutes', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 1::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range starts on start of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
select tstzrange(lower(previous), lower(previous) + interval '5 minutes', '[]'),
tstzrange(lower(previous), 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range ends on end of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
-- the resolution on a timestamptz is 1 microsecond, so subtracting one microsecond from the upper bound of
-- the month range puts us as close to the "end of month" as possible, wthout going over.
-- https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME
select tstzrange(lower(previous) + interval '5 minutes', upper(previous) - interval '1 microsecond', '[]'),
tstzrange(lower(previous) + interval '5 minutes', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range starts on end of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
-- the resolution on a timestamptz is 1 microsecond, so subtracting one microsecond from the upper bound of
-- the month range puts us as close to the "end of month" as possible, wthout going over.
-- https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME
select tstzrange(upper(previous) - interval '1 microsecond', upper(previous) + '5 minutes', '[]'),
tstzrange(upper(previous) - interval '1 microsecond', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 1::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- active_time_range ends on start of month
select lives_ok('truncate wh_auth_token_accumulating_fact');
with month_range (previous) as (
select tstzrange(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), '[)')
),
range (active, valid) as (
select tstzrange(upper(previous) - interval '5 minutes', upper(previous), '[]'),
tstzrange(upper(previous) - interval '5 minutes', 'infinity'::timestamptz)
from month_range
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'at____atok1', user_key.key,
wh_date_key(lower(range.valid)), wh_time_key(lower(range.valid)), lower(range.valid),
coalesce(wh_date_key(upper(range.valid)), -1), coalesce(wh_time_key(upper(range.valid)), -1), upper(range.valid),
wh_date_key(upper(range.active)), wh_time_key(upper(range.active)), upper(range.active),
range.active,
range.valid,
1
from range, user_key;
select is(count(*), 1::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 1::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- multiple tokens for a user within month count as single user
select lives_ok('truncate wh_auth_token_accumulating_fact');
with token (public_id, active) as (
select *
from (values
('at____atok1', tstzrange(date_trunc('month', now() - interval '1 month'),
date_trunc('month', now() - interval '1 month') + interval '5 minutes')),
('at____atok2', tstzrange(date_trunc('month', now() - interval '1 month') + interval '10 minutes',
date_trunc('month', now() - interval '1 month') + interval '15 minutes'))
) as t (public_id, active)
),
user_key (key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select token.public_id, user_key.key,
wh_date_key(lower(token.active)), wh_time_key(lower(token.active)), lower(token.active),
coalesce(wh_date_key(upper(token.active)), -1), coalesce(wh_time_key(upper(token.active)), -1), upper(token.active),
wh_date_key(upper(token.active)), wh_time_key(upper(token.active)), upper(token.active),
token.active,
token.active,
1
from token, user_key;
select is(count(*), 2::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 1::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 1::bigint)
$$);
-- multiple tokens for multiple users should get counted correctly
-- Create a user dimension for corresponding auth token's user.
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email
) values (
'u_____user2', 'None', 'None',
'a______acc2', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
),(
'u_____user3', 'None', 'None',
'a______acc3', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
);
select lives_ok('truncate wh_auth_token_accumulating_fact');
with token (user_id, public_id, active) as (
select *
from (values
('u_____user1', 'at___u1tok1', tstzrange(date_trunc('month', now() - interval '1 month'),
date_trunc('month', now() - interval '1 month') + interval '5 minutes')),
('u_____user1', 'at___u1tok2', tstzrange(date_trunc('month', now() - interval '1 month') + interval '10 minutes',
date_trunc('month', now() - interval '1 month') + interval '15 minutes')),
('u_____user2', 'at___u2tok1', tstzrange(date_trunc('month', now() - interval '1 month'),
date_trunc('month', now() - interval '1 month') + interval '5 minutes')),
('u_____user2', 'at___u2tok2', tstzrange(date_trunc('month', now() - interval '1 month') + interval '10 minutes',
date_trunc('month', now() - interval '1 month') + interval '15 minutes')),
('u_____user3', 'at___u3tok1', tstzrange(date_trunc('month', now() - interval '1 month'),
date_trunc('month', now() - interval '1 month') + interval '5 minutes')),
('u_____user3', 'at___u3tok2', tstzrange(date_trunc('month', now() - interval '1 month') + interval '10 minutes',
date_trunc('month', now() - interval '1 month') + interval '15 minutes')),
('u_____user3', 'at___u3tok3', tstzrange(date_trunc('month', now() - interval '1 month') + interval '10 minutes',
date_trunc('month', now() - interval '1 month') + interval '15 minutes'))
) as t (user_id, public_id, active)
),
token_key (user_key, public_id, active) as (
select whud.key, token.public_id, token.active
from wh_user_dimension as whud
join token
on whud.user_id = token.user_id
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select token_key.public_id, token_key.user_key,
wh_date_key(lower(token_key.active)), wh_time_key(lower(token_key.active)), lower(token_key.active),
coalesce(wh_date_key(upper(token_key.active)), -1), coalesce(wh_time_key(upper(token_key.active)), -1), upper(token_key.active),
wh_date_key(upper(token_key.active)), wh_time_key(upper(token_key.active)), upper(token_key.active),
token_key.active,
token_key.active,
1
from token_key;
select is(count(*), 7::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 3::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_last_2_months',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 3::bigint)
$$);
select * from finish();
rollback;
@@ -0,0 +1,100 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(4);
select lives_ok('truncate wh_auth_token_accumulating_fact,
wh_user_dimension,
wh_session_accumulating_fact,
wh_session_connection_accumulating_fact;',
'Truncate tables in preparation for testing');
with
month_range (date_key, time_key, month) as (
select wh_date_key(s), wh_time_key(s), s
from generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
users (user_id, u) as (
select 'u_____user'||u, u
from generate_series(1, 6, 1) as u
),
user_key (key, user_id) as (
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email)
select users.user_id, 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
from users
returning key, user_id
),
tokens (date_key, time_key, user_id, token_id) as (
select wh_date_key(s), wh_time_key(s), users.user_id, 't_____u'||users.u||'tok'||s as token_id
from users,
generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
tokens_user_keys (date_key, time_key, user_id, token_id, user_key) as (
select tokens.date_key, tokens.time_key, tokens.user_id, tokens.token_id, user_key.key
from tokens
join user_key
on user_key.user_id = tokens.user_id
),
auth_tokens (user_key, user_id, token_id, valid_range) as (
select tokens_user_keys.user_key, tokens_user_keys.user_id, tokens_user_keys.token_id, tstzrange(month_range.month, month_range.month + interval '5 minutes', '[)')
from tokens_user_keys
join month_range
on month_range.date_key = tokens_user_keys.date_key
and month_range.time_key = tokens_user_keys.time_key
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select auth_tokens.token_id, auth_tokens.user_key,
wh_date_key(lower(auth_tokens.valid_range)), wh_time_key(lower(auth_tokens.valid_range)), lower(auth_tokens.valid_range),
coalesce(wh_date_key(upper(auth_tokens.valid_range)), -1), coalesce(wh_time_key(upper(auth_tokens.valid_range)), -1), upper(auth_tokens.valid_range),
wh_date_key(upper(auth_tokens.valid_range)), wh_time_key(upper(auth_tokens.valid_range)), upper(auth_tokens.valid_range),
auth_tokens.valid_range,
auth_tokens.valid_range,
1
from auth_tokens;
select is(count(*), 72::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 6::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
select results_eq(
'select * from hcp_billing_monthly_active_users_all',
$$
values (date_trunc('month', now()), date_trunc('hour', now()), 0::bigint),
(date_trunc('month', now() - interval '1 month'), date_trunc('month', now()), 6::bigint),
(date_trunc('month', now() - interval '2 month'), date_trunc('month', now() - interval '1 month'), 6::bigint),
(date_trunc('month', now() - interval '3 month'), date_trunc('month', now() - interval '2 month'), 6::bigint),
(date_trunc('month', now() - interval '4 month'), date_trunc('month', now() - interval '3 month'), 6::bigint),
(date_trunc('month', now() - interval '5 month'), date_trunc('month', now() - interval '4 month'), 6::bigint),
(date_trunc('month', now() - interval '6 month'), date_trunc('month', now() - interval '5 month'), 6::bigint),
(date_trunc('month', now() - interval '7 month'), date_trunc('month', now() - interval '6 month'), 6::bigint),
(date_trunc('month', now() - interval '8 month'), date_trunc('month', now() - interval '7 month'), 6::bigint),
(date_trunc('month', now() - interval '9 month'), date_trunc('month', now() - interval '8 month'), 6::bigint),
(date_trunc('month', now() - interval '10 month'), date_trunc('month', now() - interval '9 month'), 6::bigint),
(date_trunc('month', now() - interval '11 month'), date_trunc('month', now() - interval '10 month'), 6::bigint),
(date_trunc('month', now() - interval '12 month'), date_trunc('month', now() - interval '11 month'), 6::bigint)
$$);
select * from finish();
rollback;
@@ -0,0 +1,179 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(11);
select lives_ok('truncate wh_auth_token_accumulating_fact,
wh_user_dimension,
wh_session_accumulating_fact,
wh_session_connection_accumulating_fact;',
'Truncate tables in preparation for testing');
-- generate auth tokens for users simulating the server being configured to use a different time zone.
set time zone 'NZ';
with
month_range (date_key, time_key, month) as (
select wh_date_key(s), wh_time_key(s), s
from generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
users (user_id, u) as (
select 'u_____user'||u, u
from generate_series(1, 6, 1) as u
),
user_key (key, user_id) as (
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email)
select users.user_id, 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
from users
returning key, user_id
),
tokens (date_key, time_key, user_id, token_id) as (
select wh_date_key(s), wh_time_key(s), users.user_id, 't_____u'||users.u||'tok'||s as token_id
from users,
generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
tokens_user_keys (date_key, time_key, user_id, token_id, user_key) as (
select tokens.date_key, tokens.time_key, tokens.user_id, tokens.token_id, user_key.key
from tokens
join user_key
on user_key.user_id = tokens.user_id
),
auth_tokens (user_key, user_id, token_id, valid_range) as (
select tokens_user_keys.user_key, tokens_user_keys.user_id, tokens_user_keys.token_id, tstzrange(month_range.month, month_range.month + interval '5 minutes', '[)')
from tokens_user_keys
join month_range
on month_range.date_key = tokens_user_keys.date_key
and month_range.time_key = tokens_user_keys.time_key
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select auth_tokens.token_id, auth_tokens.user_key,
wh_date_key(lower(auth_tokens.valid_range)), wh_time_key(lower(auth_tokens.valid_range)), lower(auth_tokens.valid_range),
coalesce(wh_date_key(upper(auth_tokens.valid_range)), -1), coalesce(wh_time_key(upper(auth_tokens.valid_range)), -1), upper(auth_tokens.valid_range),
wh_date_key(upper(auth_tokens.valid_range)), wh_time_key(upper(auth_tokens.valid_range)), upper(auth_tokens.valid_range),
auth_tokens.valid_range,
auth_tokens.valid_range,
1
from auth_tokens;
select is(count(*), 72::bigint, 'wh_auth_token_accumulating_fact is not empty') from wh_auth_token_accumulating_fact;
select is(count(*), 6::bigint, 'wh_user_dimension is not empty') from wh_user_dimension;
-- calling the view directly yields results at NZ month boundaries.
select results_eq(
'select * from hcp_billing_monthly_active_users_all',
$$
values (date_trunc('month', now(), 'nz'), date_trunc('hour', now(), 'nz'), 0::bigint),
(date_trunc('month', now() - interval '1 month', 'nz'), date_trunc('month', now(), 'nz'), 6::bigint),
(date_trunc('month', now() - interval '2 month', 'nz'), date_trunc('month', now() - interval '1 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '3 month', 'nz'), date_trunc('month', now() - interval '2 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '4 month', 'nz'), date_trunc('month', now() - interval '3 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '5 month', 'nz'), date_trunc('month', now() - interval '4 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '6 month', 'nz'), date_trunc('month', now() - interval '5 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '7 month', 'nz'), date_trunc('month', now() - interval '6 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '8 month', 'nz'), date_trunc('month', now() - interval '7 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '9 month', 'nz'), date_trunc('month', now() - interval '8 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '10 month', 'nz'), date_trunc('month', now() - interval '9 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '11 month', 'nz'), date_trunc('month', now() - interval '10 month', 'nz'), 6::bigint),
(date_trunc('month', now() - interval '12 month', 'nz'), date_trunc('month', now() - interval '11 month', 'nz'), 6::bigint)
$$);
-- calling the function yields results at UTC month boundaries.
select results_eq(
'select count(*) from hcp_billing_monthly_active_users_all()',
$$
values (14::bigint);
$$);
select results_eq(
'select * from hcp_billing_monthly_active_users_all()',
$$
values (date_trunc('month', now(), 'utc'), date_trunc('hour', now(), 'utc'), 0::bigint),
(date_trunc('month', now() - interval '1 month', 'utc'), date_trunc('month', now(), 'utc'), 0::bigint),
(date_trunc('month', now() - interval '2 month', 'utc'), date_trunc('month', now() - interval '1 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '3 month', 'utc'), date_trunc('month', now() - interval '2 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '4 month', 'utc'), date_trunc('month', now() - interval '3 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '5 month', 'utc'), date_trunc('month', now() - interval '4 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '6 month', 'utc'), date_trunc('month', now() - interval '5 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '7 month', 'utc'), date_trunc('month', now() - interval '6 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '8 month', 'utc'), date_trunc('month', now() - interval '7 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '9 month', 'utc'), date_trunc('month', now() - interval '8 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '10 month', 'utc'), date_trunc('month', now() - interval '9 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '11 month', 'utc'), date_trunc('month', now() - interval '10 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '12 month', 'utc'), date_trunc('month', now() - interval '11 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '13 month', 'utc'), date_trunc('month', now() - interval '12 month', 'utc'), 6::bigint)
$$);
select results_ne(
'select * from hcp_billing_monthly_active_users_all()',
'select * from hcp_billing_monthly_active_users_all');
-- can provide a start time (inclusive) to limit the results.
select results_eq(
$$
select * from hcp_billing_monthly_active_users_all(date_trunc('month', now() - interval '5 month', 'utc'));
$$,
$$
values (date_trunc('month', now(), 'utc'), date_trunc('hour', now(), 'utc'), 0::bigint),
(date_trunc('month', now() - interval '1 month', 'utc'), date_trunc('month', now(), 'utc'), 0::bigint),
(date_trunc('month', now() - interval '2 month', 'utc'), date_trunc('month', now() - interval '1 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '3 month', 'utc'), date_trunc('month', now() - interval '2 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '4 month', 'utc'), date_trunc('month', now() - interval '3 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '5 month', 'utc'), date_trunc('month', now() - interval '4 month', 'utc'), 6::bigint)
$$);
-- can provide a start time (inclusive) and end time (exclusive) to limit results.
select results_eq(
$$
select * from hcp_billing_monthly_active_users_all(date_trunc('month', now() - interval '5 month', 'utc'),
date_trunc('month', now() - interval '3 month', 'utc'));
$$,
$$
values (date_trunc('month', now() - interval '4 month', 'utc'), date_trunc('month', now() - interval '3 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '5 month', 'utc'), date_trunc('month', now() - interval '4 month', 'utc'), 6::bigint)
$$);
-- can provide an end time (exclusive) to limit results.
select results_eq(
$$
select * from hcp_billing_monthly_active_users_all(null,
date_trunc('month', now() - interval '3 month', 'utc'));
$$,
$$
values (date_trunc('month', now() - interval '4 month', 'utc'), date_trunc('month', now() - interval '3 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '5 month', 'utc'), date_trunc('month', now() - interval '4 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '6 month', 'utc'), date_trunc('month', now() - interval '5 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '7 month', 'utc'), date_trunc('month', now() - interval '6 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '8 month', 'utc'), date_trunc('month', now() - interval '7 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '9 month', 'utc'), date_trunc('month', now() - interval '8 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '10 month', 'utc'), date_trunc('month', now() - interval '9 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '11 month', 'utc'), date_trunc('month', now() - interval '10 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '12 month', 'utc'), date_trunc('month', now() - interval '11 month', 'utc'), 6::bigint),
(date_trunc('month', now() - interval '13 month', 'utc'), date_trunc('month', now() - interval '12 month', 'utc'), 6::bigint)
$$);
-- an end time that is before the start time will yield no results.
select is_empty(
$$
select * from hcp_billing_monthly_active_users_all(date_trunc('month', now() - interval '2 month', 'utc'),
date_trunc('month', now() - interval '5 month', 'utc'));
$$);
select * from finish();
rollback;
@@ -3,7 +3,7 @@
begin;
select plan(13);
select plan(10);
-- Verify the trigger functions exist and are declared properly
select has_function('insert_deleted_id');
@@ -25,17 +25,6 @@ begin;
select is(count(*), 0::bigint) from auth_token where public_id = 'tok____clare';
select is(count(*), 1::bigint) from auth_token_deleted;
-- Add new auth token with duplicate public_id, delete and check that there's no error
insert into auth_token
(key_id, auth_account_id, public_id, token)
values
('kdkv__colors', 'apa____clare', 'tok____clare', 'tok____clare'::bytea);
prepare delete_again as
delete from auth_token where public_id = 'tok____clare';
select lives_ok('delete_again');
select is(count(*), 0::bigint) from auth_token where public_id = 'tok____clare';
select is(count(*), 1::bigint) from auth_token_deleted;
select * from finish();
rollback;
@@ -0,0 +1,507 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(13);
-- Create a user dimension for corresponding auth token's user.
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email
) values (
'u_____user1', 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
);
prepare insert_access_time_after_delete as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 11:00:00'::timestamptz),
wh_time_key('2023-12-13 11:00:00'::timestamptz),
'2023-12-13 11:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token1',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_access_time_after_delete',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "last_accessed_time_lte_deleted_time"'
);
prepare insert_active_time_after_issued as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 13:00:00'::timestamptz),
wh_time_key('2023-12-13 13:00:00'::timestamptz),
'2023-12-13 13:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token2',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts + interval '10 minutes', accessed_timestamp.ts),
tstzrange(issued_timestamp.ts, deleted_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_active_time_after_issued',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "active_time_lower_eq_issued_time"'
);
prepare insert_active_time_before_access as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 13:00:00'::timestamptz),
wh_time_key('2023-12-13 13:00:00'::timestamptz),
'2023-12-13 13:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token2',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts - interval '10 minutes'),
tstzrange(issued_timestamp.ts, deleted_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_active_time_before_access',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "active_time_upper_eq_last_accessed_time"'
);
prepare insert_active_not_contained_by_valid as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 11:00:00'::timestamptz),
wh_time_key('2023-12-13 11:00:00'::timestamptz),
'2023-12-13 11:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token1',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
tstzrange(issued_timestamp.ts, deleted_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_active_not_contained_by_valid',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "active_time_contained_by_valid_time"'
);
prepare insert_valid_time_before_issued as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 13:00:00'::timestamptz),
wh_time_key('2023-12-13 13:00:00'::timestamptz),
'2023-12-13 13:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token2',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
tstzrange(issued_timestamp.ts - interval '10 minutes', deleted_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_valid_time_before_issued',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "valid_time_lower_eq_issued"'
);
prepare insert_valid_time_before_deleted as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 13:00:00'::timestamptz),
wh_time_key('2023-12-13 13:00:00'::timestamptz),
'2023-12-13 13:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token2',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
tstzrange(issued_timestamp.ts, deleted_timestamp.ts - interval '10 minutes'),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select throws_ok(
'insert_valid_time_before_deleted',
'23514',
'new row for relation "wh_auth_token_accumulating_fact" violates check constraint "valid_time_upper_eq_deleted"'
);
prepare insert_success as
with
issued_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 10:00:00'::timestamptz),
wh_time_key('2023-12-13 10:00:00'::timestamptz),
'2023-12-13 10:00:00'::timestamptz
),
accessed_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 12:00:00'::timestamptz),
wh_time_key('2023-12-13 12:00:00'::timestamptz),
'2023-12-13 12:00:00'::timestamptz
),
deleted_timestamp(date_dim_key, time_dim_key, ts) as (
select wh_date_key('2023-12-13 13:00:00'::timestamptz),
wh_time_key('2023-12-13 13:00:00'::timestamptz),
'2023-12-13 13:00:00'::timestamptz
),
user_dim(key) as (
select key
from wh_user_dimension
where user_id = 'u_____user1'
)
insert into wh_auth_token_accumulating_fact (
auth_token_id,
user_key,
auth_token_issued_date_key,
auth_token_issued_time_key,
auth_token_issued_time,
auth_token_deleted_date_key,
auth_token_deleted_time_key,
auth_token_deleted_time,
auth_token_approximate_last_access_date_key,
auth_token_approximate_last_access_time_key,
auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select 'tok___token2',
user_dim.key,
issued_timestamp.date_dim_key,
issued_timestamp.time_dim_key,
issued_timestamp.ts,
deleted_timestamp.date_dim_key,
deleted_timestamp.time_dim_key,
deleted_timestamp.ts,
accessed_timestamp.date_dim_key,
accessed_timestamp.time_dim_key,
accessed_timestamp.ts,
tstzrange(issued_timestamp.ts, accessed_timestamp.ts),
tstzrange(issued_timestamp.ts, deleted_timestamp.ts),
1
from user_dim,
issued_timestamp,
deleted_timestamp,
accessed_timestamp;
select lives_ok('insert_success');
-- ensure user_id was properly set
select is(wh_auth_token_accumulating_fact.user_id, 'u_____user1')
from wh_auth_token_accumulating_fact
where auth_token_id = 'tok___token2';
prepare update_user_id as
update wh_auth_token_accumulating_fact set user_id = 'u_____user2'
where auth_token_id = 'tok___token2';
select throws_ok(
'update_user_id',
'23601',
'immutable column: wh_auth_token_accumulating_fact.user_id'
);
prepare update_user_key as
update wh_auth_token_accumulating_fact set user_key = 'key_2'
where auth_token_id = 'tok___token2';
select throws_ok(
'update_user_key',
'23601',
'immutable column: wh_auth_token_accumulating_fact.user_key'
);
prepare update_auth_token_issued_time as
update wh_auth_token_accumulating_fact set auth_token_issued_time = '2023-12-13 11:01:00'::timestamptz
where auth_token_id = 'tok___token2';
select throws_ok(
'update_auth_token_issued_time',
'23601',
'immutable column: wh_auth_token_accumulating_fact.auth_token_issued_time'
);
prepare update_auth_token_issued_time_key as
update wh_auth_token_accumulating_fact set auth_token_issued_time_key = wh_time_key('2023-12-13 11:01:00'::timestamptz)
where auth_token_id = 'tok___token2';
select throws_ok(
'update_auth_token_issued_time_key',
'23601',
'immutable column: wh_auth_token_accumulating_fact.auth_token_issued_time_key'
);
prepare update_auth_token_issued_date_key as
update wh_auth_token_accumulating_fact set auth_token_issued_date_key = wh_date_key('2023-12-14 11:00:00'::timestamptz)
where auth_token_id = 'tok___token2';
select throws_ok(
'update_auth_token_issued_date_key',
'23601',
'immutable column: wh_auth_token_accumulating_fact.auth_token_issued_date_key'
);
select * from finish();
rollback;
@@ -0,0 +1,75 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(14);
-- should have an auth token for carly
select is(count(*), 1::bigint)
from auth_token
where public_id = 'tok____carly';
-- should have one fact for carly.
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- the fact should not have a deleted time.
select is(auth_token_deleted_date_key, -1)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_deleted_time_key, -1)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_deleted_time, 'infinity'::wh_timestamp)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(upper(auth_token_valid_time_range), 'infinity'::timestamptz)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- Now delete the auth token and confirm the fact was updated properly.
delete
from auth_token
where public_id = 'tok____carly';
-- should still have one fact
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- times should be updated to have a deleted time.
select is(auth_token_deleted_date_key, wh_date_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_deleted_time_key, wh_time_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_deleted_time, now()::wh_timestamp)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(upper(auth_token_valid_time_range), now())
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- should have an auth token for cora that is in pending status
select is(count(*), 1::bigint)
from auth_token
where public_id = 'tok_____cora'
and status = 'auth token pending';
-- since the auth token is pending, there should be no fact for cora.
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_______cora';
-- Now delete the pending auth token, this should not result in a fact.
delete
from auth_token
where public_id = 'tok_____cora';
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_______cora';
select * from finish();
rollback;
@@ -0,0 +1,81 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(4);
select wtt_load('widgets', 'iam', 'kms');
-- Create a user dimension for corresponding auth token's user.
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email
) values (
'u_____user1', 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
);
-- Create auth method and account to use for making auth tokens.
insert into auth_password_conf
(password_method_id, private_id)
values
('apm___widget', 'apmc__widget');
insert into auth_password_method
(scope_id, public_id, password_conf_id, name)
values
('o_____widget', 'apm___widget', 'apmc__widget', 'Widget Auth Password');
insert into auth_password_account
(auth_method_id, public_id, login_name)
values
('apm___widget', 'apa___walter', 'walter');
update auth_account set iam_user_id = 'u_____walter' where public_id = 'apa___walter';
-- should start with no facts for this user.
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_____walter';
-- Insert with status 'auth token pending' should not create a auth token fact.
insert into auth_token
(key_id, auth_account_id, public_id, token, status)
values
('kdkv___widget', 'apa___walter', 'tok___walter1', 'tok___walter1'::bytea, 'auth token pending');
select is(count(*), 0::bigint) from wh_auth_token_accumulating_fact where user_id = 'u_____walter';
-- Insert an auth token wuth the 'token issued' status.
insert into auth_token
(key_id, auth_account_id, public_id, token, status)
values
('kdkv___widget', 'apa___walter', 'tok___walter2', 'tok___walter2'::bytea, 'token issued');
-- should have one fact for this user.
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_____walter';
select is(
wh_auth_token_accumulating_fact.*,
row('tok___walter2',
'u_____walter', (select key from wh_user_dimension where user_id = 'u_____walter'),
wh_date_key(now()), wh_time_key(now()), now(),
-1, -1, 'infinity'::timestamptz,
wh_date_key(now()), wh_time_key(now()), now(),
tstzrange(now(), now(), '[]'),
tstzrange(now(), 'infinity'::timestamptz, '[]'),
1
)::wh_auth_token_accumulating_fact
)
from wh_auth_token_accumulating_fact
where user_id = 'u_____walter';
select * from finish();
rollback;
@@ -0,0 +1,114 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(20);
-- should have an auth token for carly
select is(count(*), 1::bigint)
from auth_token
where public_id = 'tok____carly';
-- should have one fact for carly.
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- the fact should have an access time that is less than now.
select cmp_ok(auth_token_approximate_last_access_date_key, '<=', wh_date_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select cmp_ok(auth_token_approximate_last_access_time_key, '<', wh_time_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select cmp_ok(auth_token_approximate_last_access_time, '<', now()::wh_timestamp)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select cmp_ok(lower(auth_token_approximate_active_time_range), '<', now())
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select cmp_ok(upper(auth_token_approximate_active_time_range), '<', now())
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- update access time for carly's auth token.
update auth_token
set approximate_last_access_time = now()
where public_id = 'tok____carly';
-- should still only have one fact for carly.
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- the fact should have an access time of now
select is(auth_token_approximate_last_access_date_key, wh_date_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_approximate_last_access_time_key, wh_time_key(now()))
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(auth_token_approximate_last_access_time, now()::wh_timestamp)
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select cmp_ok(lower(auth_token_approximate_active_time_range), '<', now())
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
select is(upper(auth_token_approximate_active_time_range), now())
from wh_auth_token_accumulating_fact
where user_id = 'u______carly';
-- should have an auth token for cora that is in pending status
select is(count(*), 1::bigint)
from auth_token
where public_id = 'tok_____cora'
and status = 'auth token pending';
-- since the auth token is pending, there should be no fact for cora.
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_______cora';
-- updating a pending token to issued should result in a fact for cora.
update auth_token
set status = 'token issued'
where public_id = 'tok_____cora';
select is(count(*), 1::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u_______cora';
select is(
wh_auth_token_accumulating_fact.*,
row('tok_____cora',
'u_______cora', (select key from wh_user_dimension where user_id = 'u_______cora'),
wh_date_key(now()), wh_time_key(now()), now(),
-1, -1, 'infinity'::timestamptz,
wh_date_key(now()), wh_time_key(now()), now(),
tstzrange(now(), now(), '[]'),
tstzrange(now(), 'infinity'::timestamptz, '[]'),
1
)::wh_auth_token_accumulating_fact
)
from wh_auth_token_accumulating_fact
where user_id = 'u_______cora';
-- should have an auth token for ciara that is in pending status
select is(count(*), 1::bigint)
from auth_token
where public_id = 'tok____ciara'
and status = 'auth token pending';
-- since the auth token is pending, there should be no fact for ciara.
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______ciara';
-- updating a pending token to something other than issued should not result in a fact.
update auth_token
set status = 'authentication failed'
where public_id = 'tok____ciara';
select is(count(*), 0::bigint)
from wh_auth_token_accumulating_fact
where user_id = 'u______ciara';
select * from finish();
rollback;
@@ -9,12 +9,12 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 0::bigint) from wh_user_dimension where user_id = 'u_____wilson';
select lives_ok($$select wh_upsert_user('u_____walter', 'tok___walter')$$);
select lives_ok($$select wh_upsert_user('tok___wilson')$$);
-- upsert should insert a user_dimension
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____wilson';
select * from finish();
rollback;
@@ -9,7 +9,7 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 0::bigint) from wh_user_dimension where user_id = 'u_____wilson';
insert into wh_user_dimension
(
@@ -25,8 +25,8 @@ begin;
values
(
'wud_____1',
'u_____walter', 'Walter', 'This is Walter',
'apa___walter', 'password auth account', 'walter', 'Account for Walter',
'u_____wilson', 'Wilson', 'This is Wilson',
'apa___wilson', 'password auth account', 'wilson', 'Account for Wilson',
'None', 'None', 'None',
'apm___widget', 'password auth method', 'Widget Auth Password', 'None',
'None',
@@ -34,11 +34,11 @@ begin;
'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz
);
select lives_ok($$select wh_upsert_user('u_____walter', 'tok___walter')$$);
select lives_ok($$select wh_upsert_user('tok___wilson')$$);
-- upsert should insert a user_dimension
select is(count(*), 2::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(count(*), 2::bigint) from wh_user_dimension where user_id = 'u_____wilson';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____wilson' and current_row_indicator = 'Current';
select * from finish();
rollback;
@@ -1,15 +1,23 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
-- oidc_auth_new_session tests the wh_user_dimesion when
-- a new session is created using the oidc auth method.
-- ldap_auth_new_session tests the wh_user_dimesion when
-- a new session is created using the ldap auth method.
begin;
select plan(40);
select plan(41);
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimension from auth tokens
select is(count(*), 2::bigint)
from wh_user_dimension
where user_id in ('u_____walter', 'u_____warren')
and auth_method_id = 'alm___widget';
-- update walter and warren so the user dimension is changed when the session is inserted
update iam_user
set description = 'LDAP Auth New Session'
where public_id in ('u_____walter', 'u_____warren');
-- insert first session, should result in a new user dimension
insert into session
@@ -21,30 +29,43 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
-- inserting the session should result in a new user dimension.
select is(count(*), 1::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'alm___widget'
and user_description = 'LDAP Auth New Session';
select is(auth_account_id, 'ala___walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_name, 'walter account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_description, 'Walter LDAP Account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_external_id, 'walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_full_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_email, 'walter@widget.test') from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_description, 'LDAP Auth New Session') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_id, 'ala___walter') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_name, 'walter account') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_description, 'Walter LDAP Account') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_external_id, 'walter') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_full_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_email, 'walter@widget.test') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(session_fact.user_key, user_dim.key)
from wh_session_connection_accumulating_fact as session_fact,
wh_user_dimension as user_dim
where session_fact.session_id = 's1____walter'
and user_dim.user_id = 'u_____walter';
-- insert session without full name or email
insert into session
@@ -56,29 +77,29 @@ begin;
values
('s1____warren', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren';
select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_description, 'LDAP Auth New Session') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_id, 'ala___warren') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_name, 'warren account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_description, 'Warren LDAP Account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_external_id, 'warren') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_full_name, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_email, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_id, 'ala___warren') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_name, 'warren account') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_description, 'Warren LDAP Account') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_external_id, 'warren') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_full_name, 'None') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_account_email, 'None') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____warren';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____warren' and auth_method_id = 'alm___widget' and user_description = 'LDAP Auth New Session';
select * from finish();
rollback;
@@ -8,8 +8,16 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimension from auth tokens
select is(count(*), 2::bigint)
from wh_user_dimension
where user_id in ('u_____walter', 'u_____warren')
and auth_method_id = 'aom___widget';
-- update walter and warren so the user dimension is changed when the session is inserted
update iam_user
set description = 'OIDC Auth New Session'
where public_id in ('u_____walter', 'u_____warren');
-- insert first session, should result in a new user dimension
insert into session
@@ -21,30 +29,30 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_description, 'OIDC Auth New Session') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_id, 'aoa___walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_type, 'oidc auth account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_name, 'walter account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_description, 'Walter OIDC Account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_external_id, 'sub___walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_full_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_email, 'walter@widget.test') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_id, 'aoa___walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_type, 'oidc auth account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_name, 'walter account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_description, 'Walter OIDC Account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_external_id, 'sub___walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_full_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_account_email, 'walter@widget.test') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_method_id, 'aom___widget') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_type, 'oidc auth method') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_name, 'Widget OIDC') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_external_id, 'https://oidc.widget.test') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_id, 'aom___widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_method_type, 'oidc auth method') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_method_name, 'Widget OIDC') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(auth_method_external_id, 'https://oidc.widget.test') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'OIDC Auth New Session';
-- insert session without full name or email
insert into session
@@ -56,29 +64,29 @@ begin;
values
('s1____warren', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren';
select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(user_description, 'OIDC Auth New Session') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_id, 'aoa___warren') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_type, 'oidc auth account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_name, 'warren account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_description, 'Warren OIDC Account') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_external_id, 'sub___warren') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_full_name, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_email, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_account_id, 'aoa___warren') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_type, 'oidc auth account') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_name, 'warren account') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_description, 'Warren OIDC Account') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_external_id, 'sub___warren') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_full_name, 'None') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_account_email, 'None') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_method_id, 'aom___widget') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_type, 'oidc auth method') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_name, 'Widget OIDC') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_external_id, 'https://oidc.widget.test') from wh_user_dimension where user_id = 'u_____warren';
select is(auth_method_id, 'aom___widget') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_method_type, 'oidc auth method') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_method_name, 'Widget OIDC') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(auth_method_external_id, 'https://oidc.widget.test') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____warren';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____warren';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____warren' and user_description = 'OIDC Auth New Session';
select * from finish();
rollback;
@@ -5,12 +5,20 @@
-- sessions are created using the password auth method
-- after the auth_password_account has its description change.
begin;
select plan(40);
select plan(41);
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimension from auth tokens
select is(count(*), 1::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'apm___widget';
-- update walter so the user dimension is changed when the session is inserted
update iam_user
set description = 'Passwd Change Description'
where public_id = 'u_____walter';
-- insert first session, should result in a new user dimension
insert into session
@@ -22,36 +30,42 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
-- inserting the session should result in a new user dimension.
select is(count(*), 1::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'apm___widget'
and user_description = 'Passwd Change Description';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_description, 'Passwd Change Description') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description';
-- change auth description
update auth_password_account set
description = 'Walter Password Account'
where
public_id = 'apa___walter';
update auth_password_account
set description = 'Walter Password Account'
where public_id = 'apa___walter';
-- another session with:
-- * same user
@@ -67,29 +81,33 @@ begin;
values
('s2____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 2::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 2::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'apm___widget'
and user_description = 'Passwd Change Description';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(user_description, 'Passwd Change Description') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_description, 'Walter Password Account') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_description, 'Walter Password Account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and current_row_indicator = 'Current';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Change Description' and current_row_indicator = 'Current';
select * from finish();
rollback;
@@ -4,12 +4,20 @@
-- password_auth_new_session tests the wh_user_dimesion when
-- a new session is created using the password auth method.
begin;
select plan(21);
select plan(22);
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimensions from auth tokens
select is(count(*), 1::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'apm___widget';
-- update walter so the user dimension is changed when the session is inserted
update iam_user
set description = 'Passwd Auth New Session'
where public_id = 'u_____walter';
-- insert first session, should result in a new user dimension
insert into session
@@ -21,30 +29,37 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter';
select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
-- inserting the session should result in a new user dimension.
select is(count(*), 1::bigint)
from wh_user_dimension
where user_id = 'u_____walter'
and auth_method_id = 'apm___widget'
and user_description = 'Passwd Auth New Session';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_description, 'Passwd Auth New Session') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_account_id, 'apa___walter') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_type, 'password auth account') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_name, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_full_name, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_account_email, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter';
select is(auth_method_id, 'apm___widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_method_type, 'password auth method') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_method_name, 'Widget Auth Password') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter';
select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter' and user_description = 'Passwd Auth New Session';
select * from finish();
rollback;
@@ -8,8 +8,8 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimensions from auth tokens
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- insert first session, should result in a new user dimension
insert into session
@@ -21,7 +21,7 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- another session with:
-- * same user
@@ -37,7 +37,7 @@ begin;
values
('s2____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- another session with:
-- * same user
@@ -53,7 +53,7 @@ begin;
values
('s3____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select * from finish();
rollback;
@@ -9,8 +9,8 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimensions from auth tokens
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- insert first session, should result in a new user dimension
insert into session
@@ -22,7 +22,7 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- another session with:
-- * same user
@@ -38,7 +38,7 @@ begin;
values
('s4____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 2::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 8::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and auth_account_id = 'apa1__walter';
select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter' and auth_account_id = 'apa1__walter';
@@ -8,8 +8,10 @@ begin;
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets');
-- ensure no existing dimensions
select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
-- existing dimension via auth tokens
select is(count(*), 4::bigint)
from wh_user_dimension
where user_id = 'u_____walter';
-- insert first session, should result in a new user dimension
insert into session
@@ -21,7 +23,9 @@ begin;
values
('s1____walter', 's___1wb-sths', 'h_____wb__01');
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 4::bigint)
from wh_user_dimension
where user_id = 'u_____walter';
-- update session, should not impact wh_user_dimension
update session set
@@ -29,7 +33,9 @@ begin;
where
public_id = 's1____walter';
select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget';
select is(count(*), 4::bigint)
from wh_user_dimension
where user_id = 'u_____walter';
select * from finish();
rollback;
+8 -2
View File
@@ -37,10 +37,16 @@ func (ts *Timestamp) Scan(value any) error {
// Scan implements driver.Valuer for protobuf Timestamp.
func (ts *Timestamp) Value() (driver.Value, error) {
if ts == nil {
switch {
case ts == nil:
return nil, nil
case ts.AsTime().Equal(NegativeInfinityTS):
return "-infinity", nil
case ts.AsTime().Equal(PositiveInfinityTS):
return "infinity", nil
default:
return ts.Timestamp.AsTime(), nil
}
return ts.Timestamp.AsTime(), nil
}
// GormDataType gorm common data type (required)
+26
View File
@@ -33,6 +33,18 @@ func Test_TimestampValue(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, v, utcDate(10000, 1, 1))
})
t.Run("negativeInfinity", func(t *testing.T) {
ts := New(NegativeInfinityTS)
v, err := ts.Value()
require.NoError(t, err)
assert.Equal(t, v, "-infinity")
})
t.Run("positiveInfinity", func(t *testing.T) {
ts := New(PositiveInfinityTS)
v, err := ts.Value()
require.NoError(t, err)
assert.Equal(t, v, "infinity")
})
}
func Test_TimestampScan(t *testing.T) {
@@ -65,6 +77,20 @@ func Test_TimestampScan(t *testing.T) {
err := ts.Scan(v)
require.Nil(err)
})
t.Run("negativeInfinity", func(t *testing.T) {
v := "-infinity"
ts := Timestamp{}
err := ts.Scan(v)
require.NoError(err)
assert.Equal(ts.AsTime(), NegativeInfinityTS)
})
t.Run("positiveInfinity", func(t *testing.T) {
v := "infinity"
ts := Timestamp{}
err := ts.Scan(v)
require.NoError(err)
assert.Equal(ts.AsTime(), PositiveInfinityTS)
})
}
const maxValidSeconds = 253402300800
+71
View File
@@ -17,6 +17,9 @@
{
"name": "controller.api.services.v1.AuthTokenService"
},
{
"name": "controller.api.services.v1.BillingService"
},
{
"name": "controller.api.services.v1.CredentialLibraryService"
},
@@ -635,6 +638,39 @@
]
}
},
"/v1/billing:monthly-active-users": {
"get": {
"summary": "Returns monthly active users.",
"operationId": "BillingService_MonthlyActiveUsers",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/controller.api.services.v1.MonthlyActiveUsersResponse"
}
}
},
"parameters": [
{
"name": "start_time",
"description": "An optional start time of the billing period to query, in the format of YYYY-MM.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "end_time",
"description": "An optional end time of the billing period to query, in the format of YYYY-MM.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"controller.api.services.v1.BillingService"
]
}
},
"/v1/credential-libraries": {
"get": {
"summary": "Lists all Credential Library.",
@@ -4754,6 +4790,29 @@
},
"title": "AuthToken contains all fields related to an Auth Token resource"
},
"controller.api.resources.billing.v1.ActiveUsers": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"format": "int64",
"description": "Output only. The number of active users between the start time and end time.",
"readOnly": true
},
"start_time": {
"type": "string",
"format": "date-time",
"description": "Output only. The start time of the active users count, inclusive.",
"readOnly": true
},
"end_time": {
"type": "string",
"format": "date-time",
"description": "Output only. The end time of the active users count, exclusive.",
"readOnly": true
}
}
},
"controller.api.resources.credentiallibraries.v1.CredentialLibrary": {
"type": "object",
"properties": {
@@ -8776,6 +8835,18 @@
}
}
},
"controller.api.services.v1.MonthlyActiveUsersResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/controller.api.resources.billing.v1.ActiveUsers"
}
}
}
},
"controller.api.services.v1.ReApplyStoragePolicyResponse": {
"type": "object",
"properties": {
@@ -0,0 +1,261 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc (unknown)
// source: controller/api/services/v1/billing_service.proto
package services
import (
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options"
billing "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MonthlyActiveUsersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// An optional start time of the billing period to query, in the format of YYYY-MM.
StartTime string `protobuf:"bytes,1,opt,name=start_time,proto3" json:"start_time,omitempty" class:"public"` // @gotags: class:"public"
// An optional end time of the billing period to query, in the format of YYYY-MM.
EndTime string `protobuf:"bytes,2,opt,name=end_time,proto3" json:"end_time,omitempty" class:"public"` // @gotags: class:"public"
}
func (x *MonthlyActiveUsersRequest) Reset() {
*x = MonthlyActiveUsersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MonthlyActiveUsersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MonthlyActiveUsersRequest) ProtoMessage() {}
func (x *MonthlyActiveUsersRequest) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MonthlyActiveUsersRequest.ProtoReflect.Descriptor instead.
func (*MonthlyActiveUsersRequest) Descriptor() ([]byte, []int) {
return file_controller_api_services_v1_billing_service_proto_rawDescGZIP(), []int{0}
}
func (x *MonthlyActiveUsersRequest) GetStartTime() string {
if x != nil {
return x.StartTime
}
return ""
}
func (x *MonthlyActiveUsersRequest) GetEndTime() string {
if x != nil {
return x.EndTime
}
return ""
}
type MonthlyActiveUsersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Items []*billing.ActiveUsers `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
}
func (x *MonthlyActiveUsersResponse) Reset() {
*x = MonthlyActiveUsersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MonthlyActiveUsersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MonthlyActiveUsersResponse) ProtoMessage() {}
func (x *MonthlyActiveUsersResponse) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MonthlyActiveUsersResponse.ProtoReflect.Descriptor instead.
func (*MonthlyActiveUsersResponse) Descriptor() ([]byte, []int) {
return file_controller_api_services_v1_billing_service_proto_rawDescGZIP(), []int{1}
}
func (x *MonthlyActiveUsersResponse) GetItems() []*billing.ActiveUsers {
if x != nil {
return x.Items
}
return nil
}
var File_controller_api_services_v1_billing_service_proto protoreflect.FileDescriptor
var file_controller_api_services_v1_billing_service_proto_rawDesc = []byte{
0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c,
0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x1a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x31,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e,
0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e,
0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e,
0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0x57, 0x0a, 0x19, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65,
0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08,
0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x64, 0x0a, 0x1a, 0x4d, 0x6f, 0x6e, 0x74,
0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x2e, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x74, 0x69,
0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x32, 0xe2,
0x01, 0x0a, 0x0e, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0xcf, 0x01, 0x0a, 0x12, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74,
0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74,
0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e,
0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4a, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x52, 0x65,
0x74, 0x75, 0x72, 0x6e, 0x73, 0x20, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x20, 0x61, 0x63,
0x74, 0x69, 0x76, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x22, 0x12, 0x20, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x3a, 0x6d,
0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x2d, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x2d, 0x75, 0x73,
0x65, 0x72, 0x73, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e,
0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65,
0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_controller_api_services_v1_billing_service_proto_rawDescOnce sync.Once
file_controller_api_services_v1_billing_service_proto_rawDescData = file_controller_api_services_v1_billing_service_proto_rawDesc
)
func file_controller_api_services_v1_billing_service_proto_rawDescGZIP() []byte {
file_controller_api_services_v1_billing_service_proto_rawDescOnce.Do(func() {
file_controller_api_services_v1_billing_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_services_v1_billing_service_proto_rawDescData)
})
return file_controller_api_services_v1_billing_service_proto_rawDescData
}
var file_controller_api_services_v1_billing_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_controller_api_services_v1_billing_service_proto_goTypes = []interface{}{
(*MonthlyActiveUsersRequest)(nil), // 0: controller.api.services.v1.MonthlyActiveUsersRequest
(*MonthlyActiveUsersResponse)(nil), // 1: controller.api.services.v1.MonthlyActiveUsersResponse
(*billing.ActiveUsers)(nil), // 2: controller.api.resources.billing.v1.ActiveUsers
}
var file_controller_api_services_v1_billing_service_proto_depIdxs = []int32{
2, // 0: controller.api.services.v1.MonthlyActiveUsersResponse.items:type_name -> controller.api.resources.billing.v1.ActiveUsers
0, // 1: controller.api.services.v1.BillingService.MonthlyActiveUsers:input_type -> controller.api.services.v1.MonthlyActiveUsersRequest
1, // 2: controller.api.services.v1.BillingService.MonthlyActiveUsers:output_type -> controller.api.services.v1.MonthlyActiveUsersResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_controller_api_services_v1_billing_service_proto_init() }
func file_controller_api_services_v1_billing_service_proto_init() {
if File_controller_api_services_v1_billing_service_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_controller_api_services_v1_billing_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MonthlyActiveUsersRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_controller_api_services_v1_billing_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MonthlyActiveUsersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_api_services_v1_billing_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_controller_api_services_v1_billing_service_proto_goTypes,
DependencyIndexes: file_controller_api_services_v1_billing_service_proto_depIdxs,
MessageInfos: file_controller_api_services_v1_billing_service_proto_msgTypes,
}.Build()
File_controller_api_services_v1_billing_service_proto = out.File
file_controller_api_services_v1_billing_service_proto_rawDesc = nil
file_controller_api_services_v1_billing_service_proto_goTypes = nil
file_controller_api_services_v1_billing_service_proto_depIdxs = nil
}
@@ -0,0 +1,173 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: controller/api/services/v1/billing_service.proto
/*
Package services is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package services
import (
"context"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
var (
filter_BillingService_MonthlyActiveUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BillingService_MonthlyActiveUsers_0(ctx context.Context, marshaler runtime.Marshaler, client BillingServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MonthlyActiveUsersRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BillingService_MonthlyActiveUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.MonthlyActiveUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BillingService_MonthlyActiveUsers_0(ctx context.Context, marshaler runtime.Marshaler, server BillingServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MonthlyActiveUsersRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BillingService_MonthlyActiveUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.MonthlyActiveUsers(ctx, &protoReq)
return msg, metadata, err
}
// RegisterBillingServiceHandlerServer registers the http handlers for service BillingService to "mux".
// UnaryRPC :call BillingServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterBillingServiceHandlerFromEndpoint instead.
func RegisterBillingServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server BillingServiceServer) error {
mux.Handle("GET", pattern_BillingService_MonthlyActiveUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.BillingService/MonthlyActiveUsers", runtime.WithHTTPPathPattern("/v1/billing:monthly-active-users"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BillingService_MonthlyActiveUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BillingService_MonthlyActiveUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterBillingServiceHandlerFromEndpoint is same as RegisterBillingServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterBillingServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterBillingServiceHandler(ctx, mux, conn)
}
// RegisterBillingServiceHandler registers the http handlers for service BillingService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterBillingServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterBillingServiceHandlerClient(ctx, mux, NewBillingServiceClient(conn))
}
// RegisterBillingServiceHandlerClient registers the http handlers for service BillingService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "BillingServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "BillingServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "BillingServiceClient" to call the correct interceptors.
func RegisterBillingServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client BillingServiceClient) error {
mux.Handle("GET", pattern_BillingService_MonthlyActiveUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.BillingService/MonthlyActiveUsers", runtime.WithHTTPPathPattern("/v1/billing:monthly-active-users"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BillingService_MonthlyActiveUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BillingService_MonthlyActiveUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_BillingService_MonthlyActiveUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "billing"}, "monthly-active-users"))
)
var (
forward_BillingService_MonthlyActiveUsers_0 = runtime.ForwardResponseMessage
)
@@ -0,0 +1,122 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: controller/api/services/v1/billing_service.proto
package services
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
BillingService_MonthlyActiveUsers_FullMethodName = "/controller.api.services.v1.BillingService/MonthlyActiveUsers"
)
// BillingServiceClient is the client API for BillingService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type BillingServiceClient interface {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
MonthlyActiveUsers(ctx context.Context, in *MonthlyActiveUsersRequest, opts ...grpc.CallOption) (*MonthlyActiveUsersResponse, error)
}
type billingServiceClient struct {
cc grpc.ClientConnInterface
}
func NewBillingServiceClient(cc grpc.ClientConnInterface) BillingServiceClient {
return &billingServiceClient{cc}
}
func (c *billingServiceClient) MonthlyActiveUsers(ctx context.Context, in *MonthlyActiveUsersRequest, opts ...grpc.CallOption) (*MonthlyActiveUsersResponse, error) {
out := new(MonthlyActiveUsersResponse)
err := c.cc.Invoke(ctx, BillingService_MonthlyActiveUsers_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// BillingServiceServer is the server API for BillingService service.
// All implementations must embed UnimplementedBillingServiceServer
// for forward compatibility
type BillingServiceServer interface {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
MonthlyActiveUsers(context.Context, *MonthlyActiveUsersRequest) (*MonthlyActiveUsersResponse, error)
mustEmbedUnimplementedBillingServiceServer()
}
// UnimplementedBillingServiceServer must be embedded to have forward compatible implementations.
type UnimplementedBillingServiceServer struct {
}
func (UnimplementedBillingServiceServer) MonthlyActiveUsers(context.Context, *MonthlyActiveUsersRequest) (*MonthlyActiveUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MonthlyActiveUsers not implemented")
}
func (UnimplementedBillingServiceServer) mustEmbedUnimplementedBillingServiceServer() {}
// UnsafeBillingServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to BillingServiceServer will
// result in compilation errors.
type UnsafeBillingServiceServer interface {
mustEmbedUnimplementedBillingServiceServer()
}
func RegisterBillingServiceServer(s grpc.ServiceRegistrar, srv BillingServiceServer) {
s.RegisterService(&BillingService_ServiceDesc, srv)
}
func _BillingService_MonthlyActiveUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MonthlyActiveUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BillingServiceServer).MonthlyActiveUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: BillingService_MonthlyActiveUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BillingServiceServer).MonthlyActiveUsers(ctx, req.(*MonthlyActiveUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
// BillingService_ServiceDesc is the grpc.ServiceDesc for BillingService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var BillingService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "controller.api.services.v1.BillingService",
HandlerType: (*BillingServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "MonthlyActiveUsers",
Handler: _BillingService_MonthlyActiveUsers_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "controller/api/services/v1/billing_service.proto",
}
+1 -1
View File
@@ -932,7 +932,7 @@ func Test_AnonRestrictions(t *testing.T) {
if i == resource.Controller || i == resource.Worker {
continue
}
for j := action.Type(1); j <= action.RemoveGrantScopes; j++ {
for j := action.Type(1); j <= action.MonthlyActiveUsers; j++ {
id := "foobar"
prefixes := globals.ResourcePrefixesFromType(resource.Type(i))
if len(prefixes) > 0 {
@@ -0,0 +1,21 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
syntax = "proto3";
package controller.api.resources.billing.v1;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing;billing";
message ActiveUsers {
// Output only. The number of active users between the start time and end time.
uint32 count = 1; // @gotags: `class:"public"`
// Output only. The start time of the active users count, inclusive.
google.protobuf.Timestamp start_time = 2 [json_name = "start_time"]; // @gotags: class:"public"
// Output only. The end time of the active users count, exclusive.
google.protobuf.Timestamp end_time = 3 [json_name = "end_time"]; // @gotags: class:"public"
}
@@ -0,0 +1,36 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
syntax = "proto3";
package controller.api.services.v1;
import "controller/api/resources/billing/v1/billing.proto";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
option go_package = "github.com/hashicorp/boundary/internal/gen/controller/api/services;services";
service BillingService {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
rpc MonthlyActiveUsers(MonthlyActiveUsersRequest) returns (MonthlyActiveUsersResponse) {
option (google.api.http) = {get: "/v1/billing:monthly-active-users"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {summary: "Returns monthly active users."};
}
}
message MonthlyActiveUsersRequest {
// An optional start time of the billing period to query, in the format of YYYY-MM.
string start_time = 1 [json_name = "start_time"]; // @gotags: class:"public"
// An optional end time of the billing period to query, in the format of YYYY-MM.
string end_time = 2 [json_name = "end_time"]; // @gotags: class:"public"
}
message MonthlyActiveUsersResponse {
repeated resources.billing.v1.ActiveUsers items = 1;
}
+1
View File
@@ -19,6 +19,7 @@ import (
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
+18
View File
@@ -0,0 +1,18 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
function active_users_last_two_months() {
boundary billing monthly-active-users -format json
}
function active_users_start_time() {
boundary billing monthly-active-users -start-time=$1 -format json
}
function active_users_start_time_and_end_time() {
boundary billing monthly-active-users -start-time=$1 -end-time=$2 -format json
}
function active_users_end_time() {
boundary billing monthly-active-users -end-time=$1 -format json
}
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bats
load _auth
load _billing
load _helpers
@test "boundary/billing: can login as admin user" {
run login $DEFAULT_LOGIN
[ "$status" -eq 0 ]
}
@test "boundary/billing: admin user can get last two months" {
run active_users_last_two_months
[ "$status" -eq 0 ]
run has_status_code "$output" "200"
}
@test "boundary/billing: admin user can get report with start time" {
run active_users_start_time "2023-09"
[ "$status" -eq 0 ]
run has_status_code "$output" "200"
}
@test "boundary/billing: admin user can get report with start and end times" {
run active_users_start_time_and_end_time "2023-09" "2023-12"
[ "$status" -eq 0 ]
run has_status_code "$output" "200"
}
@test "boundary/billing: cannot get report with end time before start time" {
run active_users_start_time_and_end_time "2023-09" "2023-08"
[ "$status" -eq 1 ]
}
@test "boundary/billing: cannot get report with only end time" {
run active_users_end_time "2023-09"
[ "$status" -eq 1 ]
}
# unpriv tests
@test "boundary/billing: can login as unpriv user" {
run login $DEFAULT_UNPRIVILEGED_LOGIN
[ "$status" -eq 0 ]
}
@test "boundary/billing: default user cannot get last two months" {
run active_users_last_two_months
[ "$status" -eq 1 ]
}
@test "boundary/billing: default user cannot get report with start time" {
run active_users_start_time "2023-09"
[ "$status" -eq 1 ]
run has_status_code "$output" "200"
}
@test "boundary/billing: default user cannot get report with start and end times" {
run active_users_start_time_and_end_time "2023-09" "2023-12"
[ "$status" -eq 1 ]
run has_status_code "$output" "200"
}
+3
View File
@@ -74,6 +74,7 @@ const (
AddGrantScopes Type = 60
SetGrantScopes Type = 61
RemoveGrantScopes Type = 62
MonthlyActiveUsers Type = 63
// When adding new actions, be sure to update:
//
@@ -144,6 +145,7 @@ var Map = map[string]Type{
AddGrantScopes.String(): AddGrantScopes,
SetGrantScopes.String(): SetGrantScopes,
RemoveGrantScopes.String(): RemoveGrantScopes,
MonthlyActiveUsers.String(): MonthlyActiveUsers,
}
var DeprecatedMap = map[string]Type{
@@ -220,6 +222,7 @@ func (a Type) String() string {
"add-grant-scopes",
"set-grant-scopes",
"remove-grant-scopes",
"monthly-active-users",
}[a]
}
+7
View File
@@ -35,6 +35,7 @@ const (
Credential
StorageBucket
Policy
Billing
// NOTE: When adding a new type, be sure to update:
//
// * The Grant.validateType function and test
@@ -74,6 +75,7 @@ func (r Type) String() string {
"credential",
"storage-bucket",
"policy",
"billing",
}[r]
}
@@ -83,6 +85,8 @@ func (r Type) PluralString() string {
return "credential-libraries"
case Policy:
return "policies"
case Billing: // never pluralized
return "billing"
default:
return r.String() + "s"
}
@@ -94,6 +98,8 @@ func FromPlural(s string) (Type, bool) {
return CredentialLibrary, true
case "policies":
return Policy, true
case "billing":
return Billing, true
default:
t, ok := Map[strings.TrimSuffix(s, "s")]
return t, ok
@@ -124,6 +130,7 @@ var Map = map[string]Type{
Credential.String(): Credential,
StorageBucket.String(): StorageBucket,
Policy.String(): Policy,
Billing.String(): Billing,
}
// Parent returns the parent type for a given type; if there is no parent, it
@@ -0,0 +1,185 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc (unknown)
// source: controller/api/resources/billing/v1/billing.proto
package billing
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ActiveUsers struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Output only. The number of active users between the start time and end time.
Count uint32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty" class:"public"` // @gotags: `class:"public"`
// Output only. The start time of the active users count, inclusive.
StartTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=start_time,proto3" json:"start_time,omitempty" class:"public"` // @gotags: class:"public"
// Output only. The end time of the active users count, exclusive.
EndTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=end_time,proto3" json:"end_time,omitempty" class:"public"` // @gotags: class:"public"
}
func (x *ActiveUsers) Reset() {
*x = ActiveUsers{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_resources_billing_v1_billing_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActiveUsers) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActiveUsers) ProtoMessage() {}
func (x *ActiveUsers) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_resources_billing_v1_billing_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActiveUsers.ProtoReflect.Descriptor instead.
func (*ActiveUsers) Descriptor() ([]byte, []int) {
return file_controller_api_resources_billing_v1_billing_proto_rawDescGZIP(), []int{0}
}
func (x *ActiveUsers) GetCount() uint32 {
if x != nil {
return x.Count
}
return 0
}
func (x *ActiveUsers) GetStartTime() *timestamppb.Timestamp {
if x != nil {
return x.StartTime
}
return nil
}
func (x *ActiveUsers) GetEndTime() *timestamppb.Timestamp {
if x != nil {
return x.EndTime
}
return nil
}
var File_controller_api_resources_billing_v1_billing_proto protoreflect.FileDescriptor
var file_controller_api_resources_billing_v1_billing_proto_rawDesc = []byte{
0x0a, 0x31, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69,
0x6e, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x62, 0x69,
0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x01, 0x0a, 0x0b, 0x41, 0x63,
0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12,
0x3a, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x65,
0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x42, 0x50, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e,
0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x62, 0x69,
0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_controller_api_resources_billing_v1_billing_proto_rawDescOnce sync.Once
file_controller_api_resources_billing_v1_billing_proto_rawDescData = file_controller_api_resources_billing_v1_billing_proto_rawDesc
)
func file_controller_api_resources_billing_v1_billing_proto_rawDescGZIP() []byte {
file_controller_api_resources_billing_v1_billing_proto_rawDescOnce.Do(func() {
file_controller_api_resources_billing_v1_billing_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_resources_billing_v1_billing_proto_rawDescData)
})
return file_controller_api_resources_billing_v1_billing_proto_rawDescData
}
var file_controller_api_resources_billing_v1_billing_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_controller_api_resources_billing_v1_billing_proto_goTypes = []interface{}{
(*ActiveUsers)(nil), // 0: controller.api.resources.billing.v1.ActiveUsers
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
}
var file_controller_api_resources_billing_v1_billing_proto_depIdxs = []int32{
1, // 0: controller.api.resources.billing.v1.ActiveUsers.start_time:type_name -> google.protobuf.Timestamp
1, // 1: controller.api.resources.billing.v1.ActiveUsers.end_time:type_name -> google.protobuf.Timestamp
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_controller_api_resources_billing_v1_billing_proto_init() }
func file_controller_api_resources_billing_v1_billing_proto_init() {
if File_controller_api_resources_billing_v1_billing_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_controller_api_resources_billing_v1_billing_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActiveUsers); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_api_resources_billing_v1_billing_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_controller_api_resources_billing_v1_billing_proto_goTypes,
DependencyIndexes: file_controller_api_resources_billing_v1_billing_proto_depIdxs,
MessageInfos: file_controller_api_resources_billing_v1_billing_proto_msgTypes,
}.Build()
File_controller_api_resources_billing_v1_billing_proto = out.File
file_controller_api_resources_billing_v1_billing_proto_rawDesc = nil
file_controller_api_resources_billing_v1_billing_proto_goTypes = nil
file_controller_api_resources_billing_v1_billing_proto_depIdxs = nil
}