Compare commits

..
Author SHA1 Message Date
Todd bb02a6d0db Validate host sets when they are created or updated. (#5) 2021-11-23 08:57:08 -07:00
ToddandJeff Mitchell 6ac09ab032 README includes data regarding required disabled credential rotation (#4)
* Update Readme with details around credential rotation needing to be disabled.

* Apply suggestions from code review

Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
2021-11-18 10:01:30 -07:00
Todd d9fe0aaacb Force disable_credential_rotation to true. Validate catalog and secrets input (#3)
* Remove rotation logic.  Validate input.
2021-11-16 16:06:33 -07:00
4 changed files with 135 additions and 24 deletions

No files matched your search

+12 -14
View File
@@ -19,24 +19,22 @@ Current limitations of this plugin:
compute resource compute resource
- A client secret must be generated against a registered application with any - A client secret must be generated against a registered application with any
necessary permissions necessary permissions
- For now, credential rotation must be disabled using the `disable_credential_rotation`
attribute. There is some additional work that the Boundary team identified
that is necessary to make this a seamless experience.
## Credential Rotation ## Credential Rotation
Although credentials are stored encrypted within Boundary, by default this *NOTE*: For now credential rotation must be explicitly disabled by setting
plugin will attempt to rotate credentials when they are supplied through the `disable_credential_rotation` to true on the host catalog. When we lift this
`secrets` object on a create or update call to the host catalog resource. The restriction Boundary will manage the rotation of credentials for you.
given credentials will be used to create a new credential, and then the given
credential will be revoked. In this way, after rotation, only Boundary knows the
client secret in use by this plugin.
*NOTE*: Azure's credentials are eventually consistent and there may be Although credentials are stored encrypted within Boundary, when credential
significant delay (up to ten minutes or more) between when a credential is rotation isn't explicitly disabled, this plugin will attempt to rotate
created and is available for use, or when a credential is deleted and credentials when they are supplied through the `secrets` object on a create or
unavailable for use. At the API level, this can be true even if the Azure Portal update call to the host catalog resource. The given credentials will be used to
indicates that the credential is (or isn't) there. This delay will manifest as create a new credential, and then the given credential will be revoked. In this
authentication failures when performing actions using this plugin. If secrets way, after rotation, only Boundary knows the client secret in use by this plugin.
have just been supplied and `disable_credential_rotation` is not specified,
please be patient and try again later.
## Attributes and Secrets ## Attributes and Secrets
+2 -2
View File
@@ -21,7 +21,7 @@ type AuthorizationInfo struct {
AuthParams AuthParams AuthParams AuthParams
} }
type Attributes struct { type CatalogAttributes struct {
SubscriptionId string `mapstructure:"subscription_id"` SubscriptionId string `mapstructure:"subscription_id"`
ClientId string `mapstructure:"client_id"` ClientId string `mapstructure:"client_id"`
TenantId string `mapstructure:"tenant_id"` TenantId string `mapstructure:"tenant_id"`
@@ -55,7 +55,7 @@ func getAuthorizationInfo(hc *hostcatalogs.HostCatalog) (*AuthorizationInfo, err
} }
hcFields := hc.GetAttributes().AsMap() hcFields := hc.GetAttributes().AsMap()
var attrs Attributes var attrs CatalogAttributes
if err := mapstructure.Decode(hcFields, &attrs); err != nil { if err := mapstructure.Decode(hcFields, &attrs); err != nil {
return nil, fmt.Errorf("error decoding host catalog attribute fields: %w", err) return nil, fmt.Errorf("error decoding host catalog attribute fields: %w", err)
} }
+57 -8
View File
@@ -10,6 +10,7 @@ import (
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network" "github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
@@ -28,6 +29,10 @@ var (
_ pb.HostPluginServiceServer = (*AzurePlugin)(nil) _ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
) )
type SetAttributes struct {
Filter string `mapstructure:"filter"`
}
func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) { func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) {
authzInfo, err := getAuthorizationInfo(catalog) authzInfo, err := getAuthorizationInfo(catalog)
if err != nil { if err != nil {
@@ -53,7 +58,7 @@ func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatal
}, err }, err
} }
func (p *AzurePlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
if err := validateCatalog(req.GetCatalog()); err != nil { if err := validateCatalog(req.GetCatalog()); err != nil {
return nil, err return nil, err
} }
@@ -80,7 +85,7 @@ func (p *AzurePlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatal
}, nil }, nil
} }
func (p *AzurePlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) { func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) {
if err := validateCatalog(req.GetNewCatalog()); err != nil { if err := validateCatalog(req.GetNewCatalog()); err != nil {
return nil, err return nil, err
} }
@@ -118,6 +123,20 @@ func (p *AzurePlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatal
}, nil }, nil
} }
func (p *AzurePlugin) OnCreateSet(_ context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
if err := validateSet(req.GetSet()); err != nil {
return nil, err
}
return &pb.OnCreateSetResponse{}, nil
}
func (p *AzurePlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
if err := validateSet(req.GetNewSet()); err != nil {
return nil, err
}
return &pb.OnUpdateSetResponse{}, nil
}
func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) { func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
if len(req.GetSets()) == 0 { if len(req.GetSets()) == 0 {
// Nothing to fetch // Nothing to fetch
@@ -429,19 +448,19 @@ func validateSecrets(s *structpb.Struct) error {
return nil return nil
} }
var allowedFields = map[string]struct{}{ var allowedCatalogFields = map[string]struct{}{
constDisableCredentialRotation: {}, constDisableCredentialRotation: {},
constSubscriptionId: {}, constSubscriptionId: {},
constTenantId: {}, constTenantId: {},
constClientId: {}, constClientId: {},
} }
func validateCatalog(catalog *hostcatalogs.HostCatalog) error { func validateCatalog(c *hostcatalogs.HostCatalog) error {
if catalog == nil { if c == nil {
return status.Error(codes.InvalidArgument, "catalog is nil") return status.Error(codes.InvalidArgument, "catalog is nil")
} }
var attrs Attributes var attrs CatalogAttributes
attrMap := catalog.GetAttributes().AsMap() attrMap := c.GetAttributes().AsMap()
if err := mapstructure.Decode(attrMap, &attrs); err != nil { if err := mapstructure.Decode(attrMap, &attrs); err != nil {
return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err) return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err)
} }
@@ -460,7 +479,7 @@ func validateCatalog(catalog *hostcatalogs.HostCatalog) error {
} }
for f := range attrMap { for f := range attrMap {
if _, ok := allowedFields[f]; !ok { if _, ok := allowedCatalogFields[f]; !ok {
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field." badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
} }
} }
@@ -470,3 +489,33 @@ func validateCatalog(catalog *hostcatalogs.HostCatalog) error {
} }
return nil return nil
} }
var allowedSetFields = map[string]struct{}{
constFilter: {},
}
func validateSet(s *hostsets.HostSet) error {
if s == nil {
return status.Error(codes.InvalidArgument, "set is nil")
}
var attrs SetAttributes
attrMap := s.GetAttributes().AsMap()
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
}
badFields := make(map[string]string)
if _, ok := attrMap[constFilter]; ok && len(attrs.Filter) == 0 {
badFields["attributes.filter"] = "This field must be not empty."
}
for f := range attrMap {
if _, ok := allowedSetFields[f]; !ok {
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
}
}
if len(badFields) > 0 {
return invalidArgumentError("Invalid arguments in the new set", badFields)
}
return nil
}
+64
View File
@@ -62,6 +62,16 @@ func TestValidateCatalog(t *testing.T) {
constSubscriptionId: "sub_id", constSubscriptionId: "sub_id",
}), }),
}, },
{
name: "bad type",
catalogAttrs: wrapMap(t, map[string]interface{}{
constDisableCredentialRotation: "bad type",
constClientId: "client_id",
constTenantId: "tenant_id",
constSubscriptionId: "sub_id",
}),
wantError: true,
},
{ {
name: "nil", name: "nil",
catalogAttrs: nil, catalogAttrs: nil,
@@ -178,6 +188,60 @@ func TestValidateSecrets(t *testing.T) {
} }
} }
func TestValidateSet(t *testing.T) {
cases := []struct {
name string
setAttrs *structpb.Struct
wantError bool
}{
{
name: "success",
setAttrs: wrapMap(t, map[string]interface{}{
constFilter: "filter value",
}),
},
{
name: "nil",
setAttrs: nil,
},
{
name: "bad type",
setAttrs: wrapMap(t, map[string]interface{}{
constFilter: true,
}),
wantError: true,
},
{
name: "empty",
setAttrs: wrapMap(t, map[string]interface{}{
constFilter: "",
}),
wantError: true,
},
{
name: "unrecognized field",
setAttrs: wrapMap(t, map[string]interface{}{
constFilter: "filter value",
"unrecognized": "unrecognized",
}),
wantError: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := validateSet(&hostsets.HostSet{
Attributes: tc.setAttrs,
})
if tc.wantError {
assert.Error(t, got)
} else {
assert.NoError(t, got)
}
})
}
}
func TestCreateCatalog(t *testing.T) { func TestCreateCatalog(t *testing.T) {
ctx := context.Background() ctx := context.Background()
p := &AzurePlugin{} p := &AzurePlugin{}