From a5b24e79b9931bb20fe4efd74e0f033e9032a869 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Mon, 23 Oct 2023 17:13:10 -0700 Subject: [PATCH] handlers: add refresh token helpers Helpers for parsing, marshalling and converting types to domain types. --- .../controller/handlers/refresh_token.go | 89 +++++++ .../controller/handlers/refresh_token_test.go | 229 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 internal/daemon/controller/handlers/refresh_token.go create mode 100644 internal/daemon/controller/handlers/refresh_token_test.go diff --git a/internal/daemon/controller/handlers/refresh_token.go b/internal/daemon/controller/handlers/refresh_token.go new file mode 100644 index 000000000..9a3659b90 --- /dev/null +++ b/internal/daemon/controller/handlers/refresh_token.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package handlers + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/mr-tron/base58" + "google.golang.org/protobuf/proto" +) + +// ParseRefreshToken parses a refresh 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" + marshaled, err := base58.Decode(token) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + var tok pbs.ListRefreshToken + 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 { + return "", errors.New(ctx, errors.InvalidParameter, op, "token is required") + } + marshaled, err := proto.Marshal(token) + if err != nil { + return "", errors.Wrap(ctx, err, op) + } + return base58.Encode(marshaled), nil +} + +// RefreshTokenResourceToResource translates a protobuf refresh token resource type +// into a useable domain layer boundary resource type. +func RefreshTokenResourceToResource(rt pbs.ResourceType) resource.Type { + switch rt { + case pbs.ResourceType_RESOURCE_TYPE_ACCOUNT: + return resource.Account + case pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD: + return resource.AuthMethod + case pbs.ResourceType_RESOURCE_TYPE_AUTH_TOKEN: + return resource.AuthToken + case pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_LIBRARY: + return resource.CredentialLibrary + case pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_STORE: + return resource.CredentialStore + case pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL: + return resource.Credential + case pbs.ResourceType_RESOURCE_TYPE_GROUP: + return resource.Group + case pbs.ResourceType_RESOURCE_TYPE_HOST_CATALOG: + return resource.HostCatalog + case pbs.ResourceType_RESOURCE_TYPE_HOST_SET: + return resource.HostSet + case pbs.ResourceType_RESOURCE_TYPE_HOST: + return resource.Host + case pbs.ResourceType_RESOURCE_TYPE_MANAGED_GROUP: + return resource.ManagedGroup + case pbs.ResourceType_RESOURCE_TYPE_ROLE: + return resource.Role + case pbs.ResourceType_RESOURCE_TYPE_SCOPE: + return resource.Scope + case pbs.ResourceType_RESOURCE_TYPE_SESSION_RECORDING: + return resource.SessionRecording + case pbs.ResourceType_RESOURCE_TYPE_SESSION: + return resource.Session + case pbs.ResourceType_RESOURCE_TYPE_STORAGE_BUCKET: + return resource.StorageBucket + case pbs.ResourceType_RESOURCE_TYPE_TARGET: + return resource.Target + case pbs.ResourceType_RESOURCE_TYPE_USER: + return resource.User + case pbs.ResourceType_RESOURCE_TYPE_WORKER: + return resource.Worker + default: + return resource.Unknown + } +} diff --git a/internal/daemon/controller/handlers/refresh_token_test.go b/internal/daemon/controller/handlers/refresh_token_test.go new file mode 100644 index 000000000..8b51a2574 --- /dev/null +++ b/internal/daemon/controller/handlers/refresh_token_test.go @@ -0,0 +1,229 @@ +// 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())) + }) + } +}