internal/handlers: add list token helpers

Mostly a rename from refresh token, but also a new helper
for assembling the final list token.
pull/4202/head
Johan Brandhorst-Satzkorn 3 years ago
parent d4d856e712
commit f062a8cd75

@ -8,42 +8,82 @@ import (
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/mr-tron/base58"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
// ParseRefreshToken parses a refresh token from the input, returning
// ParseListToken parses a list token from the input, returning
// an error if the parsing fails.
func ParseRefreshToken(ctx context.Context, token string) (*pbs.ListRefreshToken, error) {
const op = "handlers.ParseRefreshToken"
func ParseListToken(ctx context.Context, token string) (*pbs.ListToken, error) {
const op = "handlers.ParseListToken"
marshaled, err := base58.Decode(token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var tok pbs.ListRefreshToken
var tok pbs.ListToken
if err := proto.Unmarshal(marshaled, &tok); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return &tok, nil
}
// MarshalRefreshToken marshals a refresh token to its string representation.
func MarshalRefreshToken(ctx context.Context, token *pbs.ListRefreshToken) (string, error) {
const op = "handlers.MarshalRefreshToken"
if token == nil {
// MarshalListToken assembles and marshals a list token to its string representation.
func MarshalListToken(ctx context.Context, token *listtoken.Token, resourceType pbs.ResourceType) (string, error) {
const op = "handlers.MarshalListToken"
switch {
case token == nil:
return "", errors.New(ctx, errors.InvalidParameter, op, "token is required")
case resourceType == pbs.ResourceType_RESOURCE_TYPE_UNSPECIFIED:
return "", errors.New(ctx, errors.InvalidParameter, op, "missing resource type")
case token.ResourceType != ListTokenResourceToResource(resourceType):
return "", errors.New(ctx, errors.Internal, op, "list token resource type does not match expected resource type")
}
marshaled, err := proto.Marshal(token)
lt := &pbs.ListToken{
CreateTime: timestamppb.New(token.CreateTime),
ResourceType: resourceType,
GrantsHash: token.GrantsHash,
}
switch st := token.Subtype.(type) {
case *listtoken.PaginationToken:
lt.Token = &pbs.ListToken_PaginationToken{
PaginationToken: &pbs.PaginationToken{
LastItemId: st.LastItemId,
LastItemCreateTime: timestamppb.New(st.LastItemCreateTime),
},
}
case *listtoken.StartRefreshToken:
lt.Token = &pbs.ListToken_StartRefreshToken{
StartRefreshToken: &pbs.StartRefreshToken{
PreviousPhaseUpperBound: timestamppb.New(st.PreviousPhaseUpperBound),
PreviousDeletedIdsTime: timestamppb.New(st.PreviousDeletedIdsTime),
},
}
case *listtoken.RefreshToken:
lt.Token = &pbs.ListToken_RefreshToken{
RefreshToken: &pbs.RefreshToken{
PhaseUpperBound: timestamppb.New(st.PhaseUpperBound),
PhaseLowerBound: timestamppb.New(st.PhaseLowerBound),
PreviousDeletedIdsTime: timestamppb.New(st.PreviousDeletedIdsTime),
LastItemId: st.LastItemId,
LastItemUpdateTime: timestamppb.New(st.LastItemUpdateTime),
},
}
default:
return "", errors.New(ctx, errors.Internal, op, "unexpected list token subtype")
}
marshaled, err := proto.Marshal(lt)
if err != nil {
return "", errors.Wrap(ctx, err, op)
}
return base58.Encode(marshaled), nil
}
// RefreshTokenResourceToResource translates a protobuf refresh token resource type
// ListTokenResourceToResource translates a protobuf list token resource type
// into a useable domain layer boundary resource type.
func RefreshTokenResourceToResource(rt pbs.ResourceType) resource.Type {
func ListTokenResourceToResource(rt pbs.ResourceType) resource.Type {
switch rt {
case pbs.ResourceType_RESOURCE_TYPE_ACCOUNT:
return resource.Account

@ -0,0 +1,382 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package handlers_test
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_ParseListToken(t *testing.T) {
t.Parallel()
pagToken := &pbs.ListToken{
CreateTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_PaginationToken{
PaginationToken: &pbs.PaginationToken{
LastItemId: "ttcp_1234567890",
LastItemCreateTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
},
},
}
pagBytes, err := proto.Marshal(pagToken)
require.NoError(t, err)
pagString := base58.Encode(pagBytes)
srToken := &pbs.ListToken{
CreateTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_StartRefreshToken{
StartRefreshToken: &pbs.StartRefreshToken{
PreviousPhaseUpperBound: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
PreviousDeletedIdsTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
},
},
}
srBytes, err := proto.Marshal(srToken)
require.NoError(t, err)
srString := base58.Encode(srBytes)
rToken := &pbs.ListToken{
CreateTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_RefreshToken{
RefreshToken: &pbs.RefreshToken{
PhaseUpperBound: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
PhaseLowerBound: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
PreviousDeletedIdsTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
LastItemId: "ttcp_1234567890",
LastItemUpdateTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
},
},
}
rBytes, err := proto.Marshal(rToken)
require.NoError(t, err)
rString := base58.Encode(rBytes)
tests := []struct {
name string
token string
want *pbs.ListToken
wantErr bool
}{
{
name: "valid pagination",
token: pagString,
want: pagToken,
},
{
name: "valid start-refresh",
token: srString,
want: srToken,
},
{
name: "valid refresh",
token: rString,
want: rToken,
},
{
name: "empty token",
token: "",
wantErr: true,
},
{
name: "invalid base58",
token: "not a token",
wantErr: true,
},
{
name: "invalid proto",
token: base58.Encode([]byte("not a token")),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := handlers.ParseListToken(context.Background(), tt.token)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
})
}
}
func Test_MarshalListToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tokenCreateTime := fiveDaysAgo
lastItemCreateTime := fiveDaysAgo.Add(time.Hour)
lastItemUpdateTime := fiveDaysAgo.Add(2 * time.Hour)
previousPhaseUpperBoundTime := fiveDaysAgo.Add(3 * time.Hour)
previousDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour)
phaseLowerBound := fiveDaysAgo.Add(5 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(6 * time.Hour)
t.Run("valid pagination", func(t *testing.T) {
pagToken := &pbs.ListToken{
CreateTime: timestamppb.New(tokenCreateTime),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_PaginationToken{
PaginationToken: &pbs.PaginationToken{
LastItemId: "ttcp_1234567890",
LastItemCreateTime: timestamppb.New(lastItemCreateTime),
},
},
}
pagBytes, err := proto.Marshal(pagToken)
require.NoError(t, err)
pagString := base58.Encode(pagBytes)
domainPagToken, err := listtoken.NewPagination(
context.Background(),
pagToken.CreateTime.AsTime(),
handlers.ListTokenResourceToResource(pagToken.ResourceType),
pagToken.GrantsHash,
pagToken.GetPaginationToken().LastItemId,
pagToken.GetPaginationToken().LastItemCreateTime.AsTime(),
)
require.NoError(t, err)
got, err := handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_TARGET)
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, pagString, protocmp.Transform()))
})
t.Run("valid start-refresh", func(t *testing.T) {
srToken := &pbs.ListToken{
CreateTime: timestamppb.New(tokenCreateTime),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_StartRefreshToken{
StartRefreshToken: &pbs.StartRefreshToken{
PreviousPhaseUpperBound: timestamppb.New(previousPhaseUpperBoundTime),
PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime),
},
},
}
srBytes, err := proto.Marshal(srToken)
require.NoError(t, err)
srString := base58.Encode(srBytes)
domainSrToken, err := listtoken.NewStartRefresh(
context.Background(),
srToken.CreateTime.AsTime(),
handlers.ListTokenResourceToResource(srToken.ResourceType),
srToken.GrantsHash,
srToken.GetStartRefreshToken().PreviousDeletedIdsTime.AsTime(),
srToken.GetStartRefreshToken().PreviousPhaseUpperBound.AsTime(),
)
require.NoError(t, err)
got, err := handlers.MarshalListToken(context.Background(), domainSrToken, pbs.ResourceType_RESOURCE_TYPE_TARGET)
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, srString, protocmp.Transform()))
})
t.Run("valid refresh", func(t *testing.T) {
rToken := &pbs.ListToken{
CreateTime: timestamppb.New(tokenCreateTime),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET,
GrantsHash: []byte("some hash"),
Token: &pbs.ListToken_RefreshToken{
RefreshToken: &pbs.RefreshToken{
PhaseUpperBound: timestamppb.New(phaseUpperBound),
PhaseLowerBound: timestamppb.New(phaseLowerBound),
PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime),
LastItemId: "ttcp_1234567890",
LastItemUpdateTime: timestamppb.New(lastItemUpdateTime),
},
},
}
rBytes, err := proto.Marshal(rToken)
require.NoError(t, err)
rString := base58.Encode(rBytes)
domainRToken, err := listtoken.NewRefresh(
context.Background(),
tokenCreateTime,
handlers.ListTokenResourceToResource(rToken.ResourceType),
rToken.GrantsHash,
rToken.GetRefreshToken().PreviousDeletedIdsTime.AsTime(),
rToken.GetRefreshToken().PhaseUpperBound.AsTime(),
rToken.GetRefreshToken().PhaseLowerBound.AsTime(),
rToken.GetRefreshToken().LastItemId,
rToken.GetRefreshToken().LastItemUpdateTime.AsTime(),
)
require.NoError(t, err)
got, err := handlers.MarshalListToken(context.Background(), domainRToken, pbs.ResourceType_RESOURCE_TYPE_TARGET)
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, rString, protocmp.Transform()))
})
t.Run("nil token", func(t *testing.T) {
_, err := handlers.MarshalListToken(context.Background(), nil, pbs.ResourceType_RESOURCE_TYPE_TARGET)
require.Error(t, err)
})
t.Run("invalid token resource type", func(t *testing.T) {
domainPagToken, err := listtoken.NewPagination(
context.Background(),
tokenCreateTime,
resource.Target,
[]byte("some hash"),
"ttcp_1234567890",
lastItemCreateTime,
)
require.NoError(t, err)
_, err = handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_UNSPECIFIED)
require.Error(t, err)
})
t.Run("token resource type mismatch", func(t *testing.T) {
domainPagToken, err := listtoken.NewPagination(
context.Background(),
tokenCreateTime,
resource.Target,
[]byte("some hash"),
"ttcp_1234567890",
lastItemCreateTime,
)
require.NoError(t, err)
_, err = handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_SESSION)
require.Error(t, err)
})
t.Run("invalid token subtype", func(t *testing.T) {
invalidToken := &listtoken.Token{
CreateTime: tokenCreateTime,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
}
_, err := handlers.MarshalListToken(context.Background(), invalidToken, pbs.ResourceType_RESOURCE_TYPE_TARGET)
require.Error(t, err)
})
}
func TestListTokenResourceToResource(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rt pbs.ResourceType
want resource.Type
}{
{
name: "default unknown",
rt: 0,
want: resource.Unknown,
},
{
name: "account",
rt: pbs.ResourceType_RESOURCE_TYPE_ACCOUNT,
want: resource.Account,
},
{
name: "auth_method",
rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD,
want: resource.AuthMethod,
},
{
name: "auth_token",
rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_TOKEN,
want: resource.AuthToken,
},
{
name: "credential_library",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_LIBRARY,
want: resource.CredentialLibrary,
},
{
name: "credential_store",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_STORE,
want: resource.CredentialStore,
},
{
name: "credential",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL,
want: resource.Credential,
},
{
name: "group",
rt: pbs.ResourceType_RESOURCE_TYPE_GROUP,
want: resource.Group,
},
{
name: "host_catalog",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST_CATALOG,
want: resource.HostCatalog,
},
{
name: "host_set",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST_SET,
want: resource.HostSet,
},
{
name: "host",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST,
want: resource.Host,
},
{
name: "managed_group",
rt: pbs.ResourceType_RESOURCE_TYPE_MANAGED_GROUP,
want: resource.ManagedGroup,
},
{
name: "role",
rt: pbs.ResourceType_RESOURCE_TYPE_ROLE,
want: resource.Role,
},
{
name: "scope",
rt: pbs.ResourceType_RESOURCE_TYPE_SCOPE,
want: resource.Scope,
},
{
name: "session_recording",
rt: pbs.ResourceType_RESOURCE_TYPE_SESSION_RECORDING,
want: resource.SessionRecording,
},
{
name: "session",
rt: pbs.ResourceType_RESOURCE_TYPE_SESSION,
want: resource.Session,
},
{
name: "storage_bucket",
rt: pbs.ResourceType_RESOURCE_TYPE_STORAGE_BUCKET,
want: resource.StorageBucket,
},
{
name: "target",
rt: pbs.ResourceType_RESOURCE_TYPE_TARGET,
want: resource.Target,
},
{
name: "user",
rt: pbs.ResourceType_RESOURCE_TYPE_USER,
want: resource.User,
},
{
name: "worker",
rt: pbs.ResourceType_RESOURCE_TYPE_WORKER,
want: resource.Worker,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := handlers.ListTokenResourceToResource(tt.rt)
require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
})
}
}

@ -1,229 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package handlers_test
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_ParseRefreshToken(t *testing.T) {
testToken := &pbs.ListRefreshToken{
CreatedTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_SESSION,
GrantsHash: []byte("some hash"),
LastItemId: "s_1234567890",
LastItemUpdatedTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
}
tokBytes, err := proto.Marshal(testToken)
require.NoError(t, err)
tokString := base58.Encode(tokBytes)
tests := []struct {
name string
token string
want *pbs.ListRefreshToken
wantErr bool
}{
{
name: "valid",
token: tokString,
want: testToken,
},
{
name: "empty token",
token: "",
wantErr: true,
},
{
name: "invalid base58",
token: "not a token",
wantErr: true,
},
{
name: "invalid proto",
token: base58.Encode([]byte("not a token")),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := handlers.ParseRefreshToken(context.Background(), tt.token)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
})
}
}
func Test_MarshalRefreshToken(t *testing.T) {
testToken := &pbs.ListRefreshToken{
CreatedTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
ResourceType: pbs.ResourceType_RESOURCE_TYPE_SESSION,
GrantsHash: []byte("some hash"),
LastItemId: "s_1234567890",
LastItemUpdatedTime: timestamppb.New(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)),
}
tokBytes, err := proto.Marshal(testToken)
require.NoError(t, err)
tokString := base58.Encode(tokBytes)
tests := []struct {
name string
token *pbs.ListRefreshToken
want string
wantErr bool
}{
{
name: "valid",
token: testToken,
want: tokString,
},
{
name: "nil token",
token: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := handlers.MarshalRefreshToken(context.Background(), tt.token)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
})
}
}
func TestRefreshTokenResourceToResource(t *testing.T) {
tests := []struct {
name string
rt pbs.ResourceType
want resource.Type
}{
{
name: "default unknown",
rt: 0,
want: resource.Unknown,
},
{
name: "account",
rt: pbs.ResourceType_RESOURCE_TYPE_ACCOUNT,
want: resource.Account,
},
{
name: "auth_method",
rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD,
want: resource.AuthMethod,
},
{
name: "auth_token",
rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_TOKEN,
want: resource.AuthToken,
},
{
name: "credential_library",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_LIBRARY,
want: resource.CredentialLibrary,
},
{
name: "credential_store",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_STORE,
want: resource.CredentialStore,
},
{
name: "credential",
rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL,
want: resource.Credential,
},
{
name: "group",
rt: pbs.ResourceType_RESOURCE_TYPE_GROUP,
want: resource.Group,
},
{
name: "host_catalog",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST_CATALOG,
want: resource.HostCatalog,
},
{
name: "host_set",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST_SET,
want: resource.HostSet,
},
{
name: "host",
rt: pbs.ResourceType_RESOURCE_TYPE_HOST,
want: resource.Host,
},
{
name: "managed_group",
rt: pbs.ResourceType_RESOURCE_TYPE_MANAGED_GROUP,
want: resource.ManagedGroup,
},
{
name: "role",
rt: pbs.ResourceType_RESOURCE_TYPE_ROLE,
want: resource.Role,
},
{
name: "scope",
rt: pbs.ResourceType_RESOURCE_TYPE_SCOPE,
want: resource.Scope,
},
{
name: "session_recording",
rt: pbs.ResourceType_RESOURCE_TYPE_SESSION_RECORDING,
want: resource.SessionRecording,
},
{
name: "session",
rt: pbs.ResourceType_RESOURCE_TYPE_SESSION,
want: resource.Session,
},
{
name: "storage_bucket",
rt: pbs.ResourceType_RESOURCE_TYPE_STORAGE_BUCKET,
want: resource.StorageBucket,
},
{
name: "target",
rt: pbs.ResourceType_RESOURCE_TYPE_TARGET,
want: resource.Target,
},
{
name: "user",
rt: pbs.ResourceType_RESOURCE_TYPE_USER,
want: resource.User,
},
{
name: "worker",
rt: pbs.ResourceType_RESOURCE_TYPE_WORKER,
want: resource.Worker,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := handlers.RefreshTokenResourceToResource(tt.rt)
require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
})
}
}
Loading…
Cancel
Save