feat: Normalize fields across various Boundary components (#5599)

Normalizes various IP/Address/Host fields across Boundary resources/components to comply with IPv6 specifications.
This commit is contained in:
Hugo authored and GitHub committed 2025-05-13 16:55:28 +01:00
1 parent 2d880326b7
commit a65d5d8573
37 files changed
+1028 -348

No files matched your search

@@ -30,6 +30,7 @@ import (
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/subtypes"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"golang.org/x/exp/maps"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/structpb"
@@ -691,7 +692,11 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item
}
attrs := item.GetOidcAccountAttributes()
if attrs.GetIssuer() != "" {
u, err := url.Parse(attrs.GetIssuer())
niss, err := parseutil.NormalizeAddr(attrs.GetIssuer())
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to normalize issuer"), errors.WithCode(errors.InvalidParameter))
}
u, err := url.Parse(niss)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse issuer"), errors.WithCode(errors.InvalidParameter))
}
@@ -1321,8 +1326,10 @@ func validateCreateRequest(ctx context.Context, req *pbs.CreateAccountRequest) e
if err != nil {
badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err)
}
if trimmed := strings.TrimSuffix(strings.TrimSuffix(du.RawPath, "/"), "/.well-known/openid-configuration"); trimmed != "" {
badFields[issuerField] = "The path segment of the url should be empty."
if du != nil {
if trimmed := strings.TrimSuffix(strings.TrimSuffix(du.RawPath, "/"), "/.well-known/openid-configuration"); trimmed != "" {
badFields[issuerField] = "The path segment of the url should be empty."
}
}
}
if attrs.GetFullName() != "" {
@@ -2424,6 +2424,41 @@ func TestCreateOidc(t *testing.T) {
},
},
},
{
name: "Create a valid Account with IPv6 issuer address",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name-ipv6-iss"},
Description: &wrapperspb.StringValue{Value: "desc-ipv6-iss"},
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
OidcAccountAttributes: &pb.OidcAccountAttributes{
Issuer: "https://[2001:BEEF:0000:0000:0000:0000:0000:0001]:44344/v1/myissuer",
Subject: "valid-account-ipv6-iss",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.OidcAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name-ipv6-iss"},
Description: &wrapperspb.StringValue{Value: "desc-ipv6-iss"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
OidcAccountAttributes: &pb.OidcAccountAttributes{
Subject: "valid-account-ipv6-iss",
Issuer: "https://[2001:beef::1]:44344/v1/myissuer",
},
},
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Create a valid Account without type defined",
req: &pbs.CreateAccountRequest{
@@ -2564,6 +2599,25 @@ func TestCreateOidc(t *testing.T) {
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Malformed issuer url",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name-ipv6-iss"},
Description: &wrapperspb.StringValue{Value: "desc-ipv6-iss"},
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
OidcAccountAttributes: &pb.OidcAccountAttributes{
Issuer: "https://2000:0005::0001]", // missing '[' after https://
Subject: "valid-account-ipv6-iss",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, `Error: "Error in provided request.", Details: {{name: "attributes.issuer", desc: "Cannot be parsed as a url. parse \"https://2000:0005::0001]\": invalid port \":0001]\" after host"}}`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -1061,8 +1061,12 @@ func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest
if err != nil {
badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err)
}
if !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) {
badFields[issuerField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https")
if iss != nil {
if !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) {
badFields[issuerField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https")
}
} else {
badFields[issuerField] = "Cannot be parsed as a url"
}
}
if attrs.GetDisableDiscoveredConfigValidation() {
@@ -19,6 +19,7 @@ import (
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/types/action"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"google.golang.org/grpc/codes"
)
@@ -419,6 +420,10 @@ func toStorageOidcAuthMethod(ctx context.Context, scopeId string, in *pb.AuthMet
// Strip off everything after and including ".well-known/openid-configuration"
// but leave the "/" attached to the end.
iss = strings.SplitN(iss, ".well-known/", 2)[0]
iss, err := parseutil.NormalizeAddr(iss)
if err != nil {
return nil, false, false, errors.Wrap(ctx, err, op, errors.WithMsg("cannot normalize issuer"), errors.WithCode(errors.InvalidParameter))
}
issuer, err := url.Parse(iss)
if err != nil {
return nil, false, false, errors.Wrap(ctx, err, op, errors.WithMsg("cannot parse issuer"), errors.WithCode(errors.InvalidParameter))
@@ -426,6 +431,10 @@ func toStorageOidcAuthMethod(ctx context.Context, scopeId string, in *pb.AuthMet
opts = append(opts, oidc.WithIssuer(issuer))
}
if apiUrl := strings.TrimSpace(attrs.GetApiUrlPrefix().GetValue()); apiUrl != "" {
apiUrl, err := parseutil.NormalizeAddr(apiUrl)
if err != nil {
return nil, false, false, errors.Wrap(ctx, err, op, errors.WithMsg("cannot normalize api_url_prefix"), errors.WithCode(errors.InvalidParameter))
}
apiU, err := url.Parse(apiUrl)
if err != nil {
return nil, false, false, errors.Wrap(ctx, err, op, errors.WithMsg("cannot parse api_url_prefix"), errors.WithCode(errors.InvalidParameter))
@@ -454,6 +454,40 @@ func TestUpdate_OIDC(t *testing.T) {
},
},
},
{
name: "Update Issuer IPv6",
req: &pbs.UpdateAuthMethodRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.issuer"},
},
Item: &pb.AuthMethod{
Attrs: func() *pb.AuthMethod_OidcAuthMethodsAttributes {
f := proto.Clone(defaultAttributes.OidcAuthMethodsAttributes).(*pb.OidcAuthMethodAttributes)
f.Issuer = wrapperspb.String("https://[2001:BEEF:0000:0000:0000:0000:0000:0001]:44344/v1/myissuer/.well-known/openid-configuration")
f.DisableDiscoveredConfigValidation = true
return &pb.AuthMethod_OidcAuthMethodsAttributes{OidcAuthMethodsAttributes: f}
}(),
},
},
res: &pbs.UpdateAuthMethodResponse{
Item: &pb.AuthMethod{
ScopeId: o.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: oidc.Subtype.String(),
Attrs: func() *pb.AuthMethod_OidcAuthMethodsAttributes {
f := proto.Clone(defaultReadAttributes.OidcAuthMethodsAttributes).(*pb.OidcAuthMethodAttributes)
f.Issuer = wrapperspb.String("https://[2001:beef::1]:44344/v1/myissuer/")
f.DisableDiscoveredConfigValidation = true
return &pb.AuthMethod_OidcAuthMethodsAttributes{OidcAuthMethodsAttributes: f}
}(),
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
AuthorizedCollectionActions: authorizedCollectionActions,
},
},
},
{
name: "invalid-issuer-port",
req: &pbs.UpdateAuthMethodRequest{
@@ -855,6 +889,38 @@ func TestUpdate_OIDC(t *testing.T) {
},
},
},
{
name: "Change Api Url Prefix IPv6",
req: &pbs.UpdateAuthMethodRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.api_url_prefix"},
},
Item: &pb.AuthMethod{
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
ApiUrlPrefix: wrapperspb.String("https://[2001:BEEF:0000:0000:0000:0000:0000:0001]:44344/path"),
},
},
},
},
res: &pbs.UpdateAuthMethodResponse{
Item: &pb.AuthMethod{
ScopeId: o.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: oidc.Subtype.String(),
Attrs: func() *pb.AuthMethod_OidcAuthMethodsAttributes {
f := proto.Clone(defaultReadAttributes.OidcAuthMethodsAttributes).(*pb.OidcAuthMethodAttributes)
f.ApiUrlPrefix = wrapperspb.String("https://[2001:beef::1]:44344/path")
f.CallbackUrl = "https://[2001:beef::1]:44344/path/v1/auth-methods/oidc:authenticate:callback"
return &pb.AuthMethod_OidcAuthMethodsAttributes{OidcAuthMethodsAttributes: f}
}(),
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
AuthorizedCollectionActions: authorizedCollectionActions,
},
},
},
{
name: "Change Allowed Audiences",
req: &pbs.UpdateAuthMethodRequest{
@@ -1127,9 +1193,7 @@ func TestUpdate_OIDC(t *testing.T) {
if got.Item.GetOidcAuthMethodsAttributes().CallbackUrl != "" {
exp := tc.res.Item.GetOidcAuthMethodsAttributes().GetCallbackUrl()
gVal := got.Item.GetOidcAuthMethodsAttributes().GetCallbackUrl()
matches, err := regexp.MatchString(exp, gVal)
require.NoError(err)
assert.True(matches, "%q doesn't match %q", gVal, exp)
assert.Equal(exp, gVal, "%q doesn't match %q", exp, gVal)
}
assert.EqualValues(3, got.Item.Version)
@@ -32,6 +32,7 @@ import (
"github.com/hashicorp/boundary/internal/types/subtypes"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentialstores"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -834,6 +835,13 @@ func toStorageVaultStore(ctx context.Context, scopeId string, in *pb.CredentialS
if attrs.GetWorkerFilter().GetValue() != "" {
opts = append(opts, vault.WithWorkerFilter(attrs.GetWorkerFilter().GetValue()))
}
if attrs.GetAddress().GetValue() != "" {
addr, err := parseutil.NormalizeAddr(attrs.GetAddress().GetValue())
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
attrs.Address = wrapperspb.String(addr)
}
// TODO (ICU-1478 and ICU-1479): Update the vault's interface around ca cert to match oidc's,
// accepting x509.Certificate instead of []byte
@@ -10,6 +10,7 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"slices"
"strings"
"testing"
@@ -647,6 +648,64 @@ func TestCreateVault(t *testing.T) {
},
},
},
{
name: "Create a valid vault CredentialStore IPv6 Address",
req: &pbs.CreateCredentialStoreRequest{Item: &pb.CredentialStore{
ScopeId: prj.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name-ipv6"},
Description: &wrapperspb.StringValue{Value: "desc-ipv6"},
Type: vault.Subtype.String(),
Attrs: &pb.CredentialStore_VaultCredentialStoreAttributes{
VaultCredentialStoreAttributes: &pb.VaultCredentialStoreAttributes{
Address: func() *wrapperspb.StringValue {
u, err := url.Parse(v.Addr)
require.NoError(t, err)
require.NotNil(t, u)
require.NotEmpty(t, u.Port())
require.NotEmpty(t, u.Scheme)
return wrapperspb.String(fmt.Sprintf("%s://[0000:0000:0000:0000:0000:0000:0000:0001]:%s", u.Scheme, u.Port()))
}(),
Token: wrapperspb.String(newToken()),
CaCert: wrapperspb.String(string(v.CaCert)),
ClientCertificate: wrapperspb.String(string(v.ClientCert)),
ClientCertificateKey: wrapperspb.String(string(v.ClientKey)),
},
},
}},
idPrefix: globals.VaultCredentialStorePrefix + "_",
res: &pbs.CreateCredentialStoreResponse{
Uri: fmt.Sprintf("credential-stores/%s_", globals.VaultCredentialStorePrefix),
Item: &pb.CredentialStore{
ScopeId: prj.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name-ipv6"},
Description: &wrapperspb.StringValue{Value: "desc-ipv6"},
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: vault.Subtype.String(),
Attrs: &pb.CredentialStore_VaultCredentialStoreAttributes{
VaultCredentialStoreAttributes: &pb.VaultCredentialStoreAttributes{
CaCert: wrapperspb.String(string(v.CaCert)),
Address: func() *wrapperspb.StringValue {
u, err := url.Parse(v.Addr)
require.NoError(t, err)
require.NotNil(t, u)
require.NotEmpty(t, u.Port())
require.NotEmpty(t, u.Scheme)
return wrapperspb.String(fmt.Sprintf("%s://[::1]:%s", u.Scheme, u.Port()))
}(),
TokenHmac: "<hmac>",
TokenStatus: "current",
ClientCertificate: wrapperspb.String(string(v.ClientCert)),
ClientCertificateKeyHmac: "<hmac>",
},
},
AuthorizedActions: testAuthorizedActions,
AuthorizedCollectionActions: testAuthorizedVaultCollectionActions,
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -6,7 +6,6 @@ package hosts
import (
"context"
"fmt"
"net"
"strings"
"github.com/hashicorp/boundary/globals"
@@ -773,15 +772,13 @@ func validateCreateRequest(req *pbs.CreateHostRequest) error {
len(attrs.GetAddress().GetValue()) > static.MaxHostAddressLength {
badFields[globals.AttributesAddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
} else {
_, _, err := net.SplitHostPort(attrs.GetAddress().GetValue())
switch {
case err == nil:
badFields[globals.AttributesAddressField] = "Address for static hosts does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
// Bare hostname, which we want
default:
_, port, err := util.SplitHostPort(attrs.GetAddress().GetValue())
if err != nil && !errors.Is(err, util.ErrMissingPort) {
badFields[globals.AttributesAddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
if port != "" {
badFields[globals.AttributesAddressField] = "Address for static hosts does not support a port."
}
}
}
case hostplugin.Subtype:
@@ -810,15 +807,13 @@ func validateUpdateRequest(req *pbs.UpdateHostRequest) error {
len(strings.TrimSpace(attrs.GetAddress().GetValue())) > static.MaxHostAddressLength {
badFields[globals.AttributesAddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
} else {
_, _, err := net.SplitHostPort(attrs.GetAddress().GetValue())
switch {
case err == nil:
badFields[globals.AttributesAddressField] = "Address for static hosts does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
// Bare hostname, which we want
default:
_, port, err := util.SplitHostPort(attrs.GetAddress().GetValue())
if err != nil && !errors.Is(err, util.ErrMissingPort) {
badFields[globals.AttributesAddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
if port != "" {
badFields[globals.AttributesAddressField] = "Address for static hosts does not support a port."
}
}
}
}
@@ -1284,6 +1284,36 @@ func TestCreate(t *testing.T) {
},
},
},
{
name: "Create a valid Host with IPv6 address",
req: &pbs.CreateHostRequest{Item: &pb.Host{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "name-ipv6"},
Description: &wrappers.StringValue{Value: "desc-ipv6"},
Type: "static",
Attrs: &pb.Host_StaticHostAttributes{
StaticHostAttributes: &pb.StaticHostAttributes{
Address: wrapperspb.String("2001:BEEF:0000:0000:0000:0000:0000:0001"),
},
},
}},
res: &pbs.CreateHostResponse{
Uri: fmt.Sprintf("hosts/%s_", globals.StaticHostPrefix),
Item: &pb.Host{
HostCatalogId: hc.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "name-ipv6"},
Description: &wrappers.StringValue{Value: "desc-ipv6"},
Type: "static",
Attrs: &pb.Host_StaticHostAttributes{
StaticHostAttributes: &pb.StaticHostAttributes{
Address: wrapperspb.String("2001:beef::1"),
},
},
AuthorizedActions: testAuthorizedActions[static.Subtype],
},
},
},
{
name: "no-attributes",
req: &pbs.CreateHostRequest{Item: &pb.Host{
@@ -1549,6 +1579,41 @@ func TestUpdate_Static(t *testing.T) {
},
},
},
{
name: "Update address",
req: &pbs.UpdateHostRequest{
Id: h.GetPublicId(),
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.AttributesAddressField},
},
Item: &pb.Host{
Attrs: &pb.Host_StaticHostAttributes{
StaticHostAttributes: &pb.StaticHostAttributes{
Address: wrapperspb.String("2001:BEEF:0000:0000:0000:0000:0000:0001"),
},
},
Type: "static",
},
},
res: &pbs.UpdateHostResponse{
Item: &pb.Host{
HostCatalogId: hc.GetPublicId(),
Id: h.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "default"},
Description: &wrappers.StringValue{Value: "default"},
CreatedTime: h.GetCreateTime().GetTimestamp(),
Type: "static",
Attrs: &pb.Host_StaticHostAttributes{
StaticHostAttributes: &pb.StaticHostAttributes{
Address: wrapperspb.String("2001:beef::1"),
},
},
AuthorizedActions: testAuthorizedActions[static.Subtype],
HostSetIds: []string{s.GetPublicId()},
},
},
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateHostRequest{
@@ -910,9 +910,9 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
"No host was discovered after checking target address and host sources.")
}
// Ensure we don't have a port from the address
_, err = util.ParseAddress(ctx, h)
if err != nil {
// Ensure we don't have a port from the address and that any ipv6 addresses
// are formatted properly
if h, err = util.ParseAddress(ctx, h); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error when parsing the chosen endpoint host address"))
}
@@ -1817,15 +1817,13 @@ func validateCreateRequest(req *pbs.CreateTargetRequest) error {
}
}
if address := item.GetAddress(); address != nil {
if len(address.GetValue()) < static.MinHostAddressLength ||
len(address.GetValue()) > static.MaxHostAddressLength {
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
}
_, _, err := net.SplitHostPort(address.GetValue())
_, err := util.ParseAddress(context.Background(), address.GetValue())
switch {
case err == nil:
case errors.Is(err, util.ErrInvalidAddressLength):
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
case errors.Is(err, util.ErrInvalidAddressContainsPort):
badFields[globals.AddressField] = "Address does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
default:
badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
@@ -1897,15 +1895,13 @@ func validateUpdateRequest(req *pbs.UpdateTargetRequest) error {
}
}
if address := item.GetAddress(); address != nil {
if len(address.GetValue()) < static.MinHostAddressLength ||
len(address.GetValue()) > static.MaxHostAddressLength {
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
}
_, _, err := net.SplitHostPort(address.GetValue())
_, err := util.ParseAddress(context.Background(), address.GetValue())
switch {
case err == nil:
case errors.Is(err, util.ErrInvalidAddressLength):
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
case errors.Is(err, util.ErrInvalidAddressContainsPort):
badFields[globals.AddressField] = "Address does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
default:
badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
@@ -9,7 +9,6 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"path"
"slices"
@@ -34,6 +33,7 @@ import (
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/event"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
@@ -1595,6 +1595,82 @@ func TestCreate(t *testing.T) {
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Invalid address ipv6 missing segment",
req: &pbs.CreateTargetRequest{Item: &pb.Target{
ScopeId: proj.GetPublicId(),
Name: wrapperspb.String("name1"),
Description: wrapperspb.String("desc"),
Type: tcp.Subtype.String(),
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
EgressWorkerFilter: wrapperspb.String(`type == "bar"`),
Address: wrapperspb.String("2001:BEEF:0:0:1:0:0001"),
}},
res: nil,
errStr: "Error parsing address: host contains an invalid IPv6 literal.",
},
{
name: "Invalid address ipv6 has brackets",
req: &pbs.CreateTargetRequest{Item: &pb.Target{
ScopeId: proj.GetPublicId(),
Name: wrapperspb.String("name2"),
Description: wrapperspb.String("desc"),
Type: tcp.Subtype.String(),
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
EgressWorkerFilter: wrapperspb.String(`type == "bar"`),
Address: wrapperspb.String("[2001:BEEF:0:0:0:1:0:0001]"),
}},
res: nil,
errStr: "Error parsing address: address cannot be encapsulated by brackets",
},
{
name: "Create a valid target with ipv6 address",
req: &pbs.CreateTargetRequest{Item: &pb.Target{
ScopeId: proj.GetPublicId(),
Name: wrapperspb.String("valid ipv6"),
Description: wrapperspb.String("desc"),
Type: tcp.Subtype.String(),
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
EgressWorkerFilter: wrapperspb.String(`type == "bar"`),
Address: wrapperspb.String("2001:BEEF:0:0:0:1:0:0001"),
}},
res: &pbs.CreateTargetResponse{
Uri: fmt.Sprintf("targets/%s_", globals.TcpTargetPrefix),
Item: &pb.Target{
ScopeId: proj.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: wrapperspb.String("valid ipv6"),
Description: wrapperspb.String("desc"),
Type: tcp.Subtype.String(),
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
SessionMaxSeconds: wrapperspb.UInt32(28800),
SessionConnectionLimit: wrapperspb.Int32(-1),
AuthorizedActions: testAuthorizedActions,
EgressWorkerFilter: wrapperspb.String(`type == "bar"`),
Address: wrapperspb.String("2001:beef::1:0:1"),
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -1618,7 +1694,7 @@ func TestCreate(t *testing.T) {
assert.True(errors.Is(gErr, tc.err), "CreateTarget(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
}
if tc.errStr != "" {
assert.ErrorContains(gErr, tc.errStr)
assert.ErrorContains(gErr, tc.errStr, "CreateTarget(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
}
} else {
assert.Nil(gErr, "Unexpected err: %v", gErr)
@@ -2156,45 +2232,6 @@ func TestUpdate(t *testing.T) {
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Invalid address length",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("ab"),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Invalid address w/ port",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("8.8.8.8:80"),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Invalid address not parsable",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("aaa.8bc.8.8:80:abc"),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -2247,6 +2284,226 @@ func TestUpdate(t *testing.T) {
targets.ValidateIngressWorkerFilterFn = validateIngressFn
}
func TestUpdateAddress(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
rw := db.New(conn)
iamRepo := iam.TestRepo(t, conn, wrapper)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(ctx, rw, rw, kms)
}
serversRepoFn := func() (*server.Repository, error) {
return server.NewRepository(ctx, rw, rw, kms)
}
org, proj := iam.TestScopes(t, iamRepo)
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
r := iam.TestRole(t, conn, proj.GetPublicId())
_ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId())
_ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*")
repoFn := func(o ...target.Option) (*target.Repository, error) {
return target.NewRepository(ctx, rw, rw, kms)
}
repo, err := repoFn()
require.NoError(t, err, "Couldn't create new target repo.")
ttar, err := target.New(ctx, tcp.Subtype, proj.GetPublicId(),
target.WithName("default"),
target.WithDescription("default"),
target.WithSessionMaxSeconds(1),
target.WithSessionConnectionLimit(1),
target.WithDefaultPort(2),
target.WithDefaultClientPort(3),
target.WithAddress("8.8.8.8"),
)
require.NoError(t, err)
tar, err := repo.CreateTarget(context.Background(), ttar)
require.NoError(t, err)
resetTarget := func() {
itar, err := repo.LookupTarget(context.Background(), tar.GetPublicId())
require.NoError(t, err)
tar, _, err = repo.UpdateTarget(context.Background(), tar, itar.GetVersion(),
[]string{"Name", "Description", "SessionMaxSeconds", "SessionConnectionLimit", "DefaultPort", "DefaultClientPort"})
require.NoError(t, err, "Failed to reset target.")
}
hCreated := tar.GetCreateTime().GetTimestamp().AsTime()
toMerge := &pbs.UpdateTargetRequest{
Id: tar.GetPublicId(),
}
tested, err := testService(t, context.Background(), conn, kms, wrapper)
require.NoError(t, err, "Failed to create a new host set service.")
// Ensure we are using the OSS worker filter functions. This prevents us
// from running tests in parallel.
server.TestUseCommunityFilterWorkersFn(t)
validateIngressFn := targets.ValidateIngressWorkerFilterFn
targets.ValidateIngressWorkerFilterFn = targets.IngressWorkerFilterUnsupported
cases := []struct {
name string
req *pbs.UpdateTargetRequest
res *pbs.UpdateTargetResponse
err string
}{
{
name: "Invalid address length",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("ab"),
},
},
res: nil,
err: "Address length must be between 3 and 255 characters",
},
{
name: "Invalid address w/ port",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("8.8.8.8:80"),
},
},
res: nil,
err: "Address does not support a port",
},
{
name: "Invalid address not parsable",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("aaa.8bc.8.8:80:abc"),
},
},
res: nil,
err: "Error parsing address: failed to parse address.",
},
{
name: "Update address valid ipv6",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("2001:BEEF:0:0:0:1:0:0001"),
},
},
res: &pbs.UpdateTargetResponse{
Item: &pb.Target{
Id: tar.GetPublicId(),
ScopeId: tar.GetProjectId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: wrapperspb.String("default"),
Description: wrapperspb.String("default"),
CreatedTime: tar.GetCreateTime().GetTimestamp(),
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
Type: tcp.Subtype.String(),
SessionMaxSeconds: wrapperspb.UInt32(tar.GetSessionMaxSeconds()),
SessionConnectionLimit: wrapperspb.Int32(tar.GetSessionConnectionLimit()),
AuthorizedActions: testAuthorizedActions,
Address: wrapperspb.String("2001:beef::1:0:1"),
},
},
},
{
name: "Update address invalid ipv6 with brackets",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("[2001:BEEF:0:0:0:1:0:0001]"),
},
},
err: "Error parsing address: address cannot be encapsulated by brackets.",
},
{
name: "Update address invalid ipv6 missing segment",
req: &pbs.UpdateTargetRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"address"},
},
Item: &pb.Target{
Address: wrapperspb.String("2001:BEEF:0:0:1:0:0001"),
},
},
err: "Error parsing address: host contains an invalid IPv6 literal.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
defer resetTarget()
assert, require := assert.New(t), require.New(t)
tc.req.Item.Version = tar.GetVersion()
req := proto.Clone(toMerge).(*pbs.UpdateTargetRequest)
proto.Merge(req, tc.req)
requestInfo := authpb.RequestInfo{
TokenFormat: uint32(auth.AuthTokenTypeBearer),
PublicId: at.GetPublicId(),
Token: at.GetToken(),
}
requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{})
ctx := auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
got, gErr := tested.UpdateTarget(ctx, req)
if tc.err != "" {
require.Error(gErr)
assert.ErrorContainsf(gErr, tc.err, "UpdateTarget(%+v) got error %v, wanted %v", req, gErr, tc.err)
return
}
require.NoError(gErr)
if got != nil {
assert.NotNilf(tc.res, "Expected UpdateHost response to be nil, but was %v", got)
gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime()
// Verify it is a set updated after it was created
// TODO: This is currently failing.
assert.True(gotUpdateTime.After(hCreated), "Updated target should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, hCreated)
// Clear all values which are hard to compare against.
got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil
}
if tc.res != nil {
tc.res.Item.Version = tc.req.Item.Version + 1
}
assert.Empty(cmp.Diff(
got,
tc.res,
protocmp.Transform(),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
), "UpdateTarget(%q) got response %q, wanted %q", req, got, tc.res)
})
}
// Reset worker filter funcs
targets.ValidateIngressWorkerFilterFn = validateIngressFn
}
func TestUpdate_BadVersion(t *testing.T) {
t.Parallel()
ctx := context.Background()