Compare commits

..
4 changed files with 24 additions and 135 deletions

No files matched your search

+14 -12
View File
@@ -19,22 +19,24 @@ 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
*NOTE*: For now credential rotation must be explicitly disabled by setting Although credentials are stored encrypted within Boundary, by default this
`disable_credential_rotation` to true on the host catalog. When we lift this plugin will attempt to rotate credentials when they are supplied through the
restriction Boundary will manage the rotation of credentials for you. `secrets` object on a create or update call to the host catalog resource. The
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.
Although credentials are stored encrypted within Boundary, when credential *NOTE*: Azure's credentials are eventually consistent and there may be
rotation isn't explicitly disabled, this plugin will attempt to rotate significant delay (up to ten minutes or more) between when a credential is
credentials when they are supplied through the `secrets` object on a create or created and is available for use, or when a credential is deleted and
update call to the host catalog resource. The given credentials will be used to unavailable for use. At the API level, this can be true even if the Azure Portal
create a new credential, and then the given credential will be revoked. In this indicates that the credential is (or isn't) there. This delay will manifest as
way, after rotation, only Boundary knows the client secret in use by this plugin. authentication failures when performing actions using this plugin. If secrets
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 CatalogAttributes struct { type Attributes 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 CatalogAttributes var attrs Attributes
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)
} }
+8 -57
View File
@@ -10,7 +10,6 @@ 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"
@@ -29,10 +28,6 @@ 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 {
@@ -58,7 +53,7 @@ func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatal
}, err }, err
} }
func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { func (p *AzurePlugin) OnCreateCatalog(ctx 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
} }
@@ -85,7 +80,7 @@ func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalog
}, nil }, nil
} }
func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) { func (p *AzurePlugin) OnUpdateCatalog(ctx 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
} }
@@ -123,20 +118,6 @@ func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalog
}, 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
@@ -448,19 +429,19 @@ func validateSecrets(s *structpb.Struct) error {
return nil return nil
} }
var allowedCatalogFields = map[string]struct{}{ var allowedFields = map[string]struct{}{
constDisableCredentialRotation: {}, constDisableCredentialRotation: {},
constSubscriptionId: {}, constSubscriptionId: {},
constTenantId: {}, constTenantId: {},
constClientId: {}, constClientId: {},
} }
func validateCatalog(c *hostcatalogs.HostCatalog) error { func validateCatalog(catalog *hostcatalogs.HostCatalog) error {
if c == nil { if catalog == nil {
return status.Error(codes.InvalidArgument, "catalog is nil") return status.Error(codes.InvalidArgument, "catalog is nil")
} }
var attrs CatalogAttributes var attrs Attributes
attrMap := c.GetAttributes().AsMap() attrMap := catalog.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)
} }
@@ -479,7 +460,7 @@ func validateCatalog(c *hostcatalogs.HostCatalog) error {
} }
for f := range attrMap { for f := range attrMap {
if _, ok := allowedCatalogFields[f]; !ok { if _, ok := allowedFields[f]; !ok {
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field." badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
} }
} }
@@ -489,33 +470,3 @@ func validateCatalog(c *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,16 +62,6 @@ 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,
@@ -188,60 +178,6 @@ 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{}