aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
blob: 9ff3206ee06503f9561f7510a1c5c9c03091f2bf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.application;

import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.TenantController;
import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;

import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.UUID;


/**
 * @author olaa
 */
public class MailVerifier {

    private static final Duration VERIFICATION_DEADLINE = Duration.ofDays(7);

    private final TenantController tenantController;
    private final Mailer mailer;
    private final CuratorDb curatorDb;
    private final Clock clock;
    private final MailTemplating mailTemplating;

    public MailVerifier(ConsoleUrls consoleUrls, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) {
        this.tenantController = tenantController;
        this.mailer = mailer;
        this.curatorDb = curatorDb;
        this.clock = clock;
        this.mailTemplating = new MailTemplating(consoleUrls);
    }

    public PendingMailVerification sendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) {
        if (!email.contains("@")) {
            throw new IllegalArgumentException("Invalid email address");
        }

        var verificationCode = UUID.randomUUID().toString();
        var verificationDeadline = clock.instant().plus(VERIFICATION_DEADLINE);
        var pendingMailVerification = new PendingMailVerification(tenantName, email, verificationCode, verificationDeadline, mailType);
        writePendingVerification(pendingMailVerification);
        mailer.send(mailOf(pendingMailVerification));
        return pendingMailVerification;
    }

    public Optional<PendingMailVerification> resendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) {
        var oldPendingVerification = curatorDb.listPendingMailVerifications()
                .stream()
                .filter(pendingMailVerification ->
                                pendingMailVerification.getMailAddress().equals(email) &&
                                        pendingMailVerification.getMailType().equals(mailType) &&
                                        pendingMailVerification.getTenantName().equals(tenantName)
                ).findFirst();

        if (oldPendingVerification.isEmpty())
            return Optional.empty();

        try (var lock = curatorDb.lockPendingMailVerification(oldPendingVerification.get().getVerificationCode())) {
            curatorDb.deletePendingMailVerification(oldPendingVerification.get());
        }

        return Optional.of(sendMailVerification(tenantName, email, mailType));
    }

    public boolean verifyMail(String verificationCode) {
        return curatorDb.getPendingMailVerification(verificationCode)
                .filter(pendingMailVerification -> pendingMailVerification.getVerificationDeadline().isAfter(clock.instant()))
                .map(pendingMailVerification -> {
                    var tenant = requireCloudTenant(pendingMailVerification.getTenantName());
                    var oldTenantInfo = tenant.info();
                    var updatedTenantInfo = switch (pendingMailVerification.getMailType()) {
                        case NOTIFICATIONS -> withTenantContacts(oldTenantInfo, pendingMailVerification);
                        case TENANT_CONTACT -> oldTenantInfo.withContact(oldTenantInfo.contact()
                                .withEmail(oldTenantInfo.contact().email().withVerification(true)));
                        case BILLING -> withVerifiedBillingMail(oldTenantInfo);
                    };

                    tenantController.lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
                        lockedTenant = lockedTenant.withInfo(updatedTenantInfo);
                        tenantController.store(lockedTenant);
                    });

                    try (var lock = curatorDb.lockPendingMailVerification(pendingMailVerification.getVerificationCode())) {
                        curatorDb.deletePendingMailVerification(pendingMailVerification);
                    }
                    return true;
                }).orElse(false);
    }

    private TenantInfo withTenantContacts(TenantInfo oldInfo, PendingMailVerification pendingMailVerification) {
        var newContacts = oldInfo.contacts().ofType(TenantContacts.EmailContact.class)
                .stream()
                .map(contact -> {
                    if (pendingMailVerification.getMailAddress().equals(contact.email().getEmailAddress()))
                        return contact.withEmail(contact.email().withVerification(true));
                    return contact;
                }).toList();
        return oldInfo.withContacts(new TenantContacts(newContacts));
    }

    private TenantInfo withVerifiedBillingMail(TenantInfo oldInfo) {
        var verifiedMail = oldInfo.billingContact().contact().email().withVerification(true);
        var billingContact = oldInfo.billingContact()
                .withContact(oldInfo.billingContact().contact().withEmail(verifiedMail));
        return oldInfo.withBilling(billingContact);
    }

    private void writePendingVerification(PendingMailVerification pendingMailVerification) {
        try (var lock = curatorDb.lockPendingMailVerification(pendingMailVerification.getVerificationCode())) {
            curatorDb.writePendingMailVerification(pendingMailVerification);
        }
    }

    private CloudTenant requireCloudTenant(TenantName tenantName) {
        return tenantController.get(tenantName)
                .filter(tenant -> tenant.type() == Tenant.Type.cloud)
                .map(CloudTenant.class::cast)
                .orElseThrow(() -> new IllegalStateException("Mail verification is only applicable for cloud tenants"));
    }

    private Mail mailOf(PendingMailVerification pendingMailVerification) {
        var message = mailTemplating.generateMailVerificationHtml(pendingMailVerification);
        return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message);
    }

}