Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Refactor: BatchCreateFederatedBundle RPC #1606

Merged
merged 1 commit into from
Jun 10, 2020

Conversation

marcosy
Copy link
Contributor

@marcosy marcosy commented Jun 1, 2020

Pull Request check list

  • Commit conforms to CONTRIBUTING.md?
  • Proper tests/regressions included?
  • Documentation updated?

Affected functionality

API Refactor

Description of change

Implements BatchCreateFederatedBundle() RPC

Which issue this PR fixes

Fixes: #1553

for _, b := range req.Bundle {
td, err := spiffeid.TrustDomainFromString(b.TrustDomain)
if err != nil {
log.Errorf("Trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trust domain string could be either the host part (e.g. example.org) or the SPIFFE ID URI (e.g. spiffe://example.org). We actually refer to "the name of the trust domain the bundle belongs to": https://github.com/spiffe/spire/blob/master/proto/spire-next/types/bundle.proto#L7
So I would just say in the error that the trust domain argument is not valid.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and in this king of logs it is better to add a field. with trust domain, instead of be part of error message.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency in logs we are adding Invalid request: $ERROR_MESSAGE

if err != nil {
log.Errorf("Trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain)
results = append(results, &bundle.BatchCreateFederatedBundleResponse_Result{
Status: CreateStatus(codes.InvalidArgument, "trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment about the trust domain string applies here.

for _, b := range req.Bundle {
td, err := spiffeid.TrustDomainFromString(b.TrustDomain)
if err != nil {
log.Errorf("Trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and in this king of logs it is better to add a field. with trust domain, instead of be part of error message.

for _, b := range req.Bundle {
td, err := spiffeid.TrustDomainFromString(b.TrustDomain)
if err != nil {
log.Errorf("Trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency in logs we are adding Invalid request: $ERROR_MESSAGE

results = append(results, &bundle.BatchCreateFederatedBundleResponse_Result{
Status: CreateStatus(codes.InvalidArgument, "trust domain argument is not a valid SPIFFE ID: %q", b.TrustDomain),
})
continue
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: instead to have too many continue, you can create a method to createFederatedBundles annd return errors,
then in Batch method you can basically parse that error into status and create result, that will make it cleaner

}

if s.td.Compare(td) == 0 {
log.Errorf("%q is this server own trust domain", td.String())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from here that we have a td, we may add it as a field to all logs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Errorf("%q is this server own trust domain", td.String())
log.Error("Creating a federated bundle for the server's own trust domain is not allowed")

}
applyBundleMask(protoBundle, req.OutputMask)

log.Infof("Bundle created successfully for trust domain: %q", td.String())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure we want an info log here, it can spam if we add several bundles

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably ok? It shouldn't be too noisy considering how often this is expected to happen.


return out, nil
// CreateStatus creates a proto Status
func CreateStatus(code codes.Code, format string, a ...interface{}) *types.Status {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think here is a good place for this method you have status methods on api: https://github.com/spiffe/spire/blob/master/pkg/server/api/status.go#L12

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! I forgot to remove it after it got merged

expectedLogMsgs []spiretest.LogEntry
dsError error
}{
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can add a test case where you create more than one bundle and someone fails and another not, and verify that we always keep order, and expected logs+results

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another test case to add is a mask with all false, what happens in that case? and when it is nil?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another test case when bundles is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can add a test case where you create more than one bundle and someone fails and another not, and verify that we always keep order, and expected logs+results

This scenario is covered by "Create fails if bundle already exists"

another test case to add is a mask with all false, what happens in that case? and when it is nil?

Will do

return nil, status.Errorf(codes.Unimplemented, "method BatchCreateFederatedBundle not implemented")
log := rpccontext.Logger(ctx)

results := []*bundle.BatchCreateFederatedBundleResponse_Result{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when req is emtpy? must we do nothing? or return an error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good question. I think I'd lean towards returning an empty response instead of failing.

Also, I'd probably recommend either initializing this slice to nil, or preallocating the slice to the number of incoming bundles.

require.NotNil(t, resp)
spiretest.AssertLogs(t, test.logHook.AllEntries(), tt.expectedLogMsgs)

require.Equal(t, len(tt.expectedResults), len(resp.Results))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this you can use spiretest.RequireProtoEqual

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and you have have an expected where you play with mask, to be sure response has exactly what we want

})
continue
}
resp, err := s.ds.CreateBundle(ctx, &datastore.CreateBundleRequest{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think before we call create bundle we must validate that rootca and public keys are valid

rootCas := []*common.Certificate{}
for _, certificate := range b.X509Authorities {
rootCas = append(rootCas, &common.Certificate{
DerBytes: certificate.Asn1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be validating that the incoming ASN.1 certificate data is well formed. See the helpers added in the AppendBundle PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'm thinking about moving the helpers added in AppendBundle PR to this file. And then, modify AppendBundle to use ProtoToBundle instead of the individual helpers.

Sounds good?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although the api package is where we've been sticking the *ToProto and ProtoTo* methods.


jwtSigningKeys := []*common.PublicKey{}
for _, key := range b.JwtAuthorities {
jwtSigningKeys = append(jwtSigningKeys, &common.PublicKey{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be validating that the incoming key id and pkix bytes. See the helpers added in the AppendBundle PR.

return nil, status.Errorf(codes.Unimplemented, "method BatchCreateFederatedBundle not implemented")
log := rpccontext.Logger(ctx)

results := []*bundle.BatchCreateFederatedBundleResponse_Result{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good question. I think I'd lean towards returning an empty response instead of failing.

Also, I'd probably recommend either initializing this slice to nil, or preallocating the slice to the number of incoming bundles.

}

if s.td.Compare(td) == 0 {
log.Errorf("%q is this server own trust domain", td.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Errorf("%q is this server own trust domain", td.String())
log.Error("Creating a federated bundle for the server's own trust domain is not allowed")

if s.td.Compare(td) == 0 {
log.Errorf("%q is this server own trust domain", td.String())
results = append(results, &bundle.BatchCreateFederatedBundleResponse_Result{
Status: CreateStatus(codes.InvalidArgument, "%q is this server own trust domain", td.String()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Status: CreateStatus(codes.InvalidArgument, "%q is this server own trust domain", td.String()),
Status: CreateStatus(codes.InvalidArgument, "creating a federated bundle for the server's own trust domain (%s) s not allowed", td.String()),

Comment on lines 202 to 278
if status.Code(err) == codes.AlreadyExists {
log.Error("Bundle already exists")
results = append(results, &bundle.BatchCreateFederatedBundleResponse_Result{
Status: CreateStatus(codes.AlreadyExists, "bundle already exists"),
})
continue
}
if err != nil {
log.WithError(err).Error("Unable to create bundle")
results = append(results, &bundle.BatchCreateFederatedBundleResponse_Result{
Status: CreateStatus(codes.Internal, "unable to create bundle: %v", err),
})
continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: switches are a nice way to group these to related conditions

}
applyBundleMask(protoBundle, req.OutputMask)

log.Infof("Bundle created successfully for trust domain: %q", td.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably ok? It shouldn't be too noisy considering how often this is expected to happen.

@@ -451,3 +572,37 @@ func setupServiceTest(t *testing.T) *serviceTest {

return test
}

func getValidBundle(td spiffeid.TrustDomain) *types.Bundle {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: get implies you are obtaining something that already exists

Suggested change
func getValidBundle(td spiffeid.TrustDomain) *types.Bundle {
func makeBundle(td spiffeid.TrustDomain) *types.Bundle {

}
}

func getValidCommonBundle(t *testing.T, td spiffeid.TrustDomain) *common.Bundle {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: get implies you are obtaining something that already exists

Suggested change
func getValidCommonBundle(t *testing.T, td spiffeid.TrustDomain) *common.Bundle {
func makeCommonBundle(t *testing.T, td spiffeid.TrustDomain) *common.Bundle {

@marcosy
Copy link
Contributor Author

marcosy commented Jun 4, 2020

Thank you all for the feedback! I think I fixed all the comments. Please let me know if I missed any of them 🙂

}

td, err := spiffeid.TrustDomainFromString(b.TrustDomain)
if err != nil && b.TrustDomain != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TrustDomain should be a required field on the bundle. I'm curious what case you hit where there wasn't one set that wasn't an error condition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the AppendBundle RPC the InputMask is applied before calling ProtoToBundle(...). So, it could be a case where TrustDomain flag is not set in the input mask and it is passed as empty to ProtoToBundle.

Copy link
Collaborator

@MarcosDY MarcosDY Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can apply mask after parsing? but here I got a question...
In actual scenary we does not validate fields that are not going to be updated, that make sense because if you dont want to update something you will just nil it on proto when sending it.

So maybe actuall appr calling mask before parsing make sense, but is that waht we want? (I'm scarred about issues because we get too permisive on our parsing)

}

if key.KeyId == "" {
return nil, errors.New("missing KeyId")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, errors.New("missing KeyId")
return nil, errors.New("missing key ID")

azdagron
azdagron previously approved these changes Jun 10, 2020
Copy link
Member

@azdagron azdagron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Thanks, @marcosy !

Signed-off-by: Marcos Yedro <marcosyedro@gmail.com>
@azdagron azdagron merged commit 867368b into spiffe:master Jun 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API Refactor: Implement Bundle.BatchCreateFederatedBundle RPC
4 participants