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

sc-14491 Part 2: Updates GDS endpoints #1008

Merged
merged 24 commits into from
May 11, 2023
Merged
4 changes: 2 additions & 2 deletions pkg/bff/models/v1/models.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions pkg/gds/emails/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,35 @@ func (m *EmailManager) SendVerifyContact(vasp *pb.VASP, contact *pb.Contact) (er
return nil
}

// SendVerifyContact sends a verification email to a contact.
func (m *EmailManager) SendVerifyModelContact(vasp *pb.VASP, contact *models.Contact) (err error) {
ctx := VerifyContactData{
Name: contact.Name,
VID: vasp.Id,
BaseURL: m.conf.VerifyContactBaseURL,
DirectoryID: m.conf.DirectoryID,
}

ctx.Token = contact.Token
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved

msg, err := VerifyContactEmail(
m.serviceEmail.Name, m.serviceEmail.Address,
contact.Name, contact.Email,
ctx,
)
if err != nil {
log.Error().Err(err).Msg("could not create verify contact email")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that ideally we want to do the logging as close to the handlers as possible; I realize that the existing method also did logging here, so maybe we should just create a follow on story to remove the logging in this file and push it into the handlers.

return err
}

if err = m.Send(msg); err != nil {
log.Error().Err(err).Msg("could not send verify contact email")
return err
}
contact.AppendEmailLog(string(admin.ResendVerifyContact), msg.Subject)
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

// SendReviewRequest is a shortcut for iComply verification in which we simply send
// an email to the TRISA admins and have them manually verify registrations.
func (m *EmailManager) SendReviewRequest(vasp *pb.VASP) (sent int, err error) {
Expand Down
132 changes: 93 additions & 39 deletions pkg/gds/gds.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/trisacrypto/directory/pkg"
admin "github.com/trisacrypto/directory/pkg/gds/admin/v2"
"github.com/trisacrypto/directory/pkg/gds/config"
"github.com/trisacrypto/directory/pkg/gds/secrets"
"github.com/trisacrypto/directory/pkg/models/v1"
Expand Down Expand Up @@ -174,7 +175,7 @@ func (s *GDS) Register(ctx context.Context, in *api.RegisterRequest) (out *api.R

// Retrieve email address from one of the supplied contacts.
var email string
if email = getContactEmail(vasp); email == "" {
if email = GetContactEmail(vasp); email == "" {
log.Error().Err(errors.New("no contact email address found")).Msg("incorrect access on validated VASP")
return nil, status.Error(codes.InvalidArgument, "no email address in supplied VASP contacts")
}
Expand All @@ -196,39 +197,68 @@ func (s *GDS) Register(ctx context.Context, in *api.RegisterRequest) (out *api.R
}

// Log successful registration
name, _ := vasp.Name()
log.Info().Str("name", name).Str("id", vasp.Id).Msg("registered VASP")
vaspName, _ := vasp.Name()
log.Info().Str("name", vaspName).Str("id", vasp.Id).Msg("registered VASP")

// Begin verification process by sending emails to all contacts in the VASP record.
// TODO: add to processing queue to return sooner/parallelize work
// Create the verification tokens and save the VASP back to the database
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
sent := 0
iter := models.NewContactIterator(vasp.Contacts, true, false)
for iter.Next() {
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
contact, kind := iter.Value()
if err = models.SetContactVerification(contact, secrets.CreateToken(48), false); err != nil {
vaspContact, kind := iter.Value()

// Add the secret token to the vasp contact
token := secrets.CreateToken(48)
if err = models.SetContactVerification(vaspContact, token, false); err != nil {
log.Error().Err(err).Str("contact", kind).Str("vasp", vasp.Id).Msg("could not set contact verification token")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
}
}

if err = s.db.UpdateVASP(ctx, vasp); err != nil {
log.Error().Err(err).Str("vasp", vasp.Id).Msg("could not update vasp with contact verification tokens")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
}

// Send contacts with updated tokens
var sent int
if sent, err = s.svc.email.SendVerifyContacts(vasp); err != nil {
// If there is an error sending contact verification emails, alert admins who
// can resend emails later, do not abort processing the registration.
log.Error().Err(err).Str("vasp", vasp.Id).Int("sent", sent).Msg("could not send verify contacts emails")
} else {
// Log successful contact verification emails sent
log.Info().Int("sent", sent).Msg("contact email verifications sent")
// If there does not exist a model contact associated with the vasp contact's email then create one.
var contact *models.Contact
if contact, err = s.db.RetrieveContact(ctx, vaspContact.Email); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note: we probably want to distinguish between "not found" errors and all other database errors. In the "not found" case we want to do what you're doing here, create the contact since the store told us it doesn't exist. In the second case we actually aren't sure if the contact exists, it's possible that kubernetes bounced the trtl pod or something. In that case we just want to return a status error aborted to avoid overwriting an existing contact.

We should be able to do if errors.Is(err, storeerrors.ErrEntityNotFound) (may have to import the package above) to check for the not found error, and more generally we should be doing this for all the s.db.Retrieve error handling. However, it's possible that other places in the code don't have this handling because this code is old.

contact = &models.Contact{
Email: vaspContact.Email,
Name: vaspContact.Name,
Vasps: []string{vasp.CommonName},
Token: token,
}
if _, err = s.db.CreateContact(ctx, contact); err != nil {
log.Error().Err(err).Str("contact", vaspContact.Email).Str("vasp", vasp.Id).Msg("could not create contact")
return nil, status.Error(codes.Aborted, "could not create contact")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
}
}

if err = s.db.UpdateVASP(ctx, vasp); err != nil {
log.Error().Err(err).Str("vasp", vasp.Id).Msg("could not update email logs on vasp")
return nil, status.Error(codes.Aborted, "could not update vasp record")
if !contact.Verified {
// Begin verification process by sending email to the contact created from the VASP record.
// TODO: add to processing queue to return sooner/parallelize work
if err = s.svc.email.SendVerifyModelContact(vasp, contact); err != nil {
// If there is an error sending contact verification emails, alert admins who
// can resend emails later, do not abort processing the registration.
log.Error().Err(err).Str("vasp", vasp.Id).Int("sent", sent).Msg("could not send verify contacts emails")
} else {
// Log successful contact verification emails sent
log.Info().Msg("contact email verification sent")
sent++

// Update the model contact record to update the email log
if err = s.db.UpdateContact(ctx, contact); err != nil {
log.Error().Err(err).Str("contact", contact.Email).Msg("could not update email logs on contact")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
return nil, status.Error(codes.Aborted, "could not update contact record")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
}
models.AppendEmailLog(vaspContact, string(admin.ResendVerifyContact), "verify_contact")
}
} else {
// If the model contact is verified make sure that the vasp contact is verified as well
if err = models.SetContactVerification(vaspContact, token, true); err != nil {
log.Error().Err(err).Str("contact", kind).Str("vasp", vasp.Id).Msg("could not set contact verification token")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
}
if err = s.db.UpdateVASP(ctx, vasp); err != nil {
log.Error().Err(err).Str("vasp", vasp.Id).Msg("could not update vasp with certificate request ID")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
return nil, status.Error(codes.Internal, "internal error with registration, please contact admins")
}
}
}

Expand Down Expand Up @@ -279,7 +309,7 @@ func (s *GDS) Register(ctx context.Context, in *api.RegisterRequest) (out *api.R
RegisteredDirectory: vasp.RegisteredDirectory,
CommonName: vasp.CommonName,
Status: vasp.VerificationStatus,
Message: "a verification code has been sent to contact emails, please check spam folder if it has not arrived; pkcs12 password attached, this is the only time it will be available -- do not lose!",
Message: "a verification code has been sent to the contact email, please check spam folder if it has not arrived; pkcs12 password attached, this is the only time it will be available -- do not lose!",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we revert this message back now, since we are potentially sending multiple emails again?

Pkcs12Password: password,
}
return out, nil
Expand Down Expand Up @@ -470,42 +500,66 @@ func (s *GDS) VerifyContact(ctx context.Context, in *api.VerifyContactRequest) (
return nil, status.Error(codes.NotFound, "could not find associated VASP record by ID")
}

// Search through the contacts to determine the contacts verified by the supplied token.
prevVerified := 0
found := false
prevVerified := 0
contactEmail := ""

// Search through the contacts to determine the contacts verified by the supplied token.
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
iter := models.NewContactIterator(vasp.Contacts, false, false)
for iter.Next() {
contact, kind := iter.Value()
vaspContact, kind := iter.Value()
var contact *models.Contact
if contact, err = s.db.RetrieveContact(ctx, vaspContact.Email); err != nil {
log.Warn().Err(err).Str("contact", contactEmail).Msg("could not retrieve contact")
return nil, status.Error(codes.NotFound, "could not find associated contact record by email")
}

// Get the verification status
token, verified, err := models.GetContactVerification(contact)
var token string
var verified bool
token, verified, err = models.GetContactVerification(vaspContact)
if err != nil {
log.Error().Err(err).Msg("could not retrieve verification from contact extra data field")
return nil, status.Error(codes.Aborted, "could not verify contact")
}

// If the model contact is verified make sure the vasp contact is verified
if contact.Verified {
found = true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok I think the only place we should be setting found = true is on line 539. We should be returning not found if the user supplies a token but it doesn't match any contact's verification token on the VASP. In other words, it's not possible to verify a contact twice. Note that this also means we need to send an empty string for the token on line 530 below, since verified contacts should no longer have a verification token.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we remove the found = true or is that causing issues?

prevVerified++
if err = models.SetContactVerification(vaspContact, token, true); err != nil {
log.Error().Err(err).Str("contact", kind).Str("vasp", vasp.Id).Msg("could not set contact verification token")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
}
continue
}

// Perform token check and if token matches, mark contact as verified
if token == in.Token {
found = true
log.Info().Str("vasp", vasp.Id).Str("contact", kind).Msg("contact email verified")
if err = models.SetContactVerification(contact, "", true); err != nil {
log.Error().Err(err).Msg("could not set verification on contact extra data field")
return nil, status.Error(codes.Aborted, "could not verify contact")

// Verify the vasp contact and the models contact
contact.Verified = true
if err = models.SetContactVerification(vaspContact, token, true); err != nil {
log.Error().Err(err).Str("contact", kind).Str("vasp", vasp.Id).Msg("could not set contact verification token")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
}
contactEmail = contact.Email

// Record the contact as verified in the audit log
if err := models.UpdateVerificationStatus(vasp, vasp.VerificationStatus, "contact verified", contactEmail); err != nil {
if err := models.UpdateVerificationStatus(vasp, vasp.VerificationStatus, "contact verified", contact.Email); err != nil {
log.Warn().Err(err).Msg("could not append contact verification to VASP audit log")
return nil, status.Error(codes.Aborted, "could not add new entry to VASP audit log")
}
} else if verified {
// Determine the total number of contacts previously verified, not including
// the current contact that was just verified. This will help prevent
// sending multiple emails to the TRISA Admins for review.
found = true
prevVerified++
}

// update the contact record
if err = s.db.UpdateContact(ctx, contact); err != nil {
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
log.Error().Err(err).Str("contact", contact.Email).Msg("could not update email logs on contact")
return nil, status.Error(codes.Aborted, "could not update contact record")
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Check if we haven't managed to verify the contact
Expand Down Expand Up @@ -608,8 +662,8 @@ func (s *GDS) Status(ctx context.Context, in *api.HealthCheck) (out *api.Service
// Helper Functions
//===========================================================================

// Get a valid email address from the contacts on a VASP.
func getContactEmail(vasp *pb.VASP) string {
// Get a valid email address and name from the contacts on a VASP.
func GetContactEmail(vasp *pb.VASP) string {
iter := models.NewContactIterator(vasp.Contacts, true, false)
for iter.Next() {
contact, _ := iter.Value()
Expand Down
39 changes: 13 additions & 26 deletions pkg/gds/gds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,15 @@ func (s *gdsTestSuite) TestVerifyContact() {
_, err = client.VerifyContact(ctx, request)
require.Error(err)

iter := models.NewContactIterator(charlie.Contacts, false, false)
for iter.Next() {
contact, _ := iter.Value()
s.svc.GetStore().CreateContact(ctx, &models.Contact{
Email: contact.Email,
Token: "administrative_token",
})
}

// Successful verification
request.Token = "administrative_token"
sent := time.Now()
Expand All @@ -519,35 +528,16 @@ func (s *gdsTestSuite) TestVerifyContact() {
token, err := models.GetAdminVerificationToken(vasp)
require.NoError(err)
require.NotEmpty(token)
token, verified, err := models.GetContactVerification(vasp.Contacts.Administrative)
require.NoError(err)
require.Empty(token)
require.True(verified)

// Verify a different contact
request.Token = "legal_token"
reply, err = client.VerifyContact(ctx, request)
require.NoError(err)
require.Nil(reply.Error)
require.Equal(pb.VerificationState_PENDING_REVIEW, reply.Status)
// Should only change the fields on the contact
vasp, err = s.svc.GetStore().RetrieveVASP(context.Background(), request.Id)
require.NoError(err)
require.Equal(pb.VerificationState_PENDING_REVIEW, vasp.VerificationStatus)
token, verified, err = models.GetContactVerification(vasp.Contacts.Legal)
require.NoError(err)
require.Empty(token)
require.True(verified)

// Attempt to verify an already verified contact - should fail
request.Token = "legal_token"
// Attempt to verify an already verified contact
request.Token = "administrative_token"
_, err = client.VerifyContact(ctx, request)
require.Error(err)
require.NoError(err)

// Check audit log entries
log, err := models.GetAuditLog(vasp)
require.NoError(err)
require.Len(log, 5)
require.Len(log, 4)
// Pre-existing entry for SUBMITTED
require.Equal(pb.VerificationState_SUBMITTED, log[0].CurrentState)
// Administrative contact verified
Expand All @@ -556,9 +546,6 @@ func (s *gdsTestSuite) TestVerifyContact() {
// State of the VASP changes to EMAIL_VERIFIED then PENDING_REVIEW
require.Equal(pb.VerificationState_EMAIL_VERIFIED, log[2].CurrentState)
require.Equal(pb.VerificationState_PENDING_REVIEW, log[3].CurrentState)
// Legal contact verified
require.Equal(pb.VerificationState_PENDING_REVIEW, log[4].CurrentState)
require.Equal(vasp.Contacts.Legal.Email, log[4].Source)

// Only one email should be sent to the admins
messages := []*emails.EmailMeta{
Expand Down
22 changes: 22 additions & 0 deletions pkg/models/v1/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,28 @@ func GetVASPEmailLog(vasp *pb.VASP) (emails []*EmailLogEntry, err error) {
return emails, nil
}

// Create and add a new entry to the EmailLog on the extra data on the Contact record.
func (c *Contact) AppendEmailLog(reason, subject string) {
// Contact must be non-nil.
if c == nil {
return
}

// Create the EmailLog if it is nil.
if c.EmailLog == nil {
c.EmailLog = make([]*EmailLogEntry, 0, 1)
}

// Append entry to the previous log.
entry := &EmailLogEntry{
Timestamp: time.Now().Format(time.RFC3339),
Reason: reason,
Subject: subject,
Recipient: c.Email,
}
c.EmailLog = append(c.EmailLog, entry)
}

// Normalize the email and convert to bytes
func NormalizeEmail(email string) string {
trimmed := strings.TrimSpace(email)
Expand Down
Loading