Skip to content

Commit

Permalink
sc-14491 Part 2: Updates GDS endpoints (#1008)
Browse files Browse the repository at this point in the history
* initial work

* changes based on PR review

* fixed comment

* updated GDS endpoints

* tests working

* more PR review based changes

* more PR review changes

* fixed build issue

* fixed other build issue

* fixed import

* fixed lint

* more PR review changes

* updated documentation

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* Update pkg/gds/gds.go

Co-authored-by: Patrick Deziel <[email protected]>

* pr review changes

---------

Co-authored-by: Patrick Deziel <[email protected]>
  • Loading branch information
Daniel Sollis and pdeziel authored May 11, 2023
1 parent fbb4e71 commit c5a7d39
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 118 deletions.
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

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")
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)
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
137 changes: 99 additions & 38 deletions pkg/gds/gds.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ 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"
"github.com/trisacrypto/directory/pkg/store"
storeerrors "github.com/trisacrypto/directory/pkg/store/errors"
"github.com/trisacrypto/directory/pkg/utils"
api "github.com/trisacrypto/trisa/pkg/trisa/gds/api/v1beta1"
pb "github.com/trisacrypto/trisa/pkg/trisa/gds/models/v1beta1"
Expand Down Expand Up @@ -174,7 +176,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 +198,74 @@ 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
sent := 0
iter := models.NewContactIterator(vasp.Contacts, true, false)
for iter.Next() {
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
contact, err = s.db.RetrieveContact(ctx, vaspContact.Email)
if err != nil {
if errors.Is(err, storeerrors.ErrEntityNotFound) {
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")
}
} else {
log.Warn().Err(err).Msg("could not register contact in database")
return nil, status.Error(codes.AlreadyExists, "could not complete registration")
}
}

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")
return nil, status.Error(codes.Aborted, "could not send contact verification emails")
}
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 contact verification on vasp")
return nil, status.Error(codes.Internal, "internal error with registration, please contact admins")
}
}
}

Expand Down Expand Up @@ -279,7 +316,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!",
Pkcs12Password: password,
}
return out, nil
Expand Down Expand Up @@ -470,40 +507,64 @@ 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.
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
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 verify contact")
}
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")

// Verify and update the models contact
contact.Verified = true
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")
}

// Verify the vasp contact
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 verify contact")
}
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++
}
}
Expand Down Expand Up @@ -608,8 +669,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

0 comments on commit c5a7d39

Please sign in to comment.