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:
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()
|
||||
|
||||
Reference in new issue
Block a user