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
111 changes: 61 additions & 50 deletions pkg/gds/gds.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,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 @@ -195,40 +195,38 @@ func (s *GDS) Register(ctx context.Context, in *api.RegisterRequest) (out *api.R
return nil, status.Error(codes.AlreadyExists, "could not complete registration, uniqueness constraints violated")
}

// Create the contact that maps to the vasp.
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
contact := &models.Contact{
Email: email,
Name: vasp.Entity.Person().GetLegalPerson().Name.String(),
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
Vasps: []string{vasp.CommonName},
}
s.db.CreateContact(ctx, contact)
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved

// Log successful registration
name, _ := vasp.Name()
log.Info().Str("name", name).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
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 {
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")
}
contact.Token = secrets.CreateToken(48)
contact.Verified = false
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved
s.db.UpdateContact(ctx, contact)
DanielSollis marked this conversation as resolved.
Show resolved Hide resolved

// Send contacts with updated tokens
var sent int
if sent, err = s.svc.email.SendVerifyContacts(vasp); err != nil {
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().Int("sent", sent).Msg("contact email verifications sent")
log.Info().Msg("contact email verification sent")

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 err = s.db.UpdateContact(ctx, contact); err != nil {
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")
}
}

Expand Down Expand Up @@ -279,7 +277,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,50 +468,63 @@ func (s *GDS) VerifyContact(ctx context.Context, in *api.VerifyContactRequest) (
return nil, status.Error(codes.NotFound, "could not find associated VASP record by ID")
}

// Retrieve email address from one of the supplied contacts.
var email string
if email = GetContactEmail(vasp); email == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we still need to iterate through the VASP's contacts to determine which contact(s) to mark verified? GetContactEmail only returns the first email in the contact order, but we still want to have a way to verify the other contacts right? For example, if the technical contact is verified then this endpoint would always return "contact already verified" which means that even with the right token we wouldn't be able to verify the other contacts.

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")
}

var contact *models.Contact
if contact, err = s.db.RetrieveContact(ctx, email); err != nil {
log.Warn().Err(err).Str("email", email).Msg("could not retrieve contact")
return nil, status.Error(codes.NotFound, "could not find associated contact record by email")
}

if contact.Verified {
log.Warn().Err(err).Str("email", email).Msg("contact already verified")
return nil, status.Error(codes.AlreadyExists, "contact record associated with vasp email is already verified")
}

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

iter := models.NewContactIterator(vasp.Contacts, false, false)
for iter.Next() {
contact, kind := iter.Value()
// Get the verification status
token, verified, err := models.GetContactVerification(contact)
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")
}
// Get the verification status
token := contact.Token
verified := contact.Verified

// 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")
}
contactEmail = contact.Email
// Perform token check and if token matches, mark contact as verified
if token == in.Token {
found = true
contact.Verified = true
contactEmail = contact.Email

// Record the contact as verified in the audit log
if err := models.UpdateVerificationStatus(vasp, vasp.VerificationStatus, "contact verified", contactEmail); 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.
prevVerified++
// Record the contact as verified in the audit log
if err := models.UpdateVerificationStatus(vasp, vasp.VerificationStatus, "contact verified", contactEmail); 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.
prevVerified++
}

// Check if we haven't managed to verify the contact
if !found {
log.Warn().Bool("found", found).Str("vasp", vasp.Id).Msg("could not find contact with token")
log.Warn().Bool("found", found).Str("contact", email).Msg("could not find contact with token")
return nil, status.Error(codes.NotFound, "could not find contact with the specified token")
}

// update the contact record
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")
return nil, status.Error(codes.Aborted, "could not update contact record")
}

// Ensures that we only send the verification email to the admins once.
// If we have previously verified contacts, assume that we've already sent the
// registration review email and do nothing.
Expand Down Expand Up @@ -609,7 +620,7 @@ func (s *GDS) Status(ctx context.Context, in *api.HealthCheck) (out *api.Service
//===========================================================================

// Get a valid email address from the contacts on a VASP.
func getContactEmail(vasp *pb.VASP) string {
func GetContactEmail(vasp *pb.VASP) string {
iter := models.NewContactIterator(vasp.Contacts, true, false)
for iter.Next() {
contact, _ := iter.Value()
Expand Down
55 changes: 7 additions & 48 deletions pkg/gds/gds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,30 +169,6 @@ func (s *gdsTestSuite) TestRegister() {
messages := []*emails.EmailMeta{
{
Contact: v.Contacts.Administrative,
To: v.Contacts.Administrative.Email,
From: s.svc.GetConf().Email.ServiceEmail,
Subject: emails.VerifyContactRE,
Reason: "verify_contact",
Timestamp: sent,
},
{
Contact: v.Contacts.Billing,
To: v.Contacts.Billing.Email,
From: s.svc.GetConf().Email.ServiceEmail,
Subject: emails.VerifyContactRE,
Reason: "verify_contact",
Timestamp: sent,
},
{
Contact: v.Contacts.Legal,
To: v.Contacts.Legal.Email,
From: s.svc.GetConf().Email.ServiceEmail,
Subject: emails.VerifyContactRE,
Reason: "verify_contact",
Timestamp: sent,
},
{
Contact: v.Contacts.Technical,
To: v.Contacts.Technical.Email,
From: s.svc.GetConf().Email.ServiceEmail,
Subject: emails.VerifyContactRE,
Expand Down Expand Up @@ -498,6 +474,11 @@ func (s *gdsTestSuite) TestVerifyContact() {
_, err = client.VerifyContact(ctx, request)
require.Error(err)

s.svc.GetStore().CreateContact(ctx, &models.Contact{
Email: gds.GetContactEmail(charlie),
Token: "administrative_token",
})

// Incorrect token - no verified contacts
request.Id = charlie.Id
request.Token = "invalid"
Expand All @@ -519,35 +500,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"
request.Token = "administrative_token"
_, err = client.VerifyContact(ctx, request)
require.Error(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 +518,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
Loading