aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
blob: afb0b61c23a21c38a98f22aa5a0f156222e84187 (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
// Copyright Yahoo. 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.organization.Mail;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
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 com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;

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

import static com.yahoo.yolean.Exceptions.uncheck;


/**
 * @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 URI dashboardUri;

    public MailVerifier(URI dashboardUri, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) {
        this.tenantController = tenantController;
        this.mailer = mailer;
        this.curatorDb = curatorDb;
        this.clock = clock;
        this.dashboardUri = dashboardUri;
    }

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

                    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 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 classLoader = this.getClass().getClassLoader();
        var template = uncheck(() -> classLoader.getResourceAsStream("mail/mail-verification.tmpl").readAllBytes());
        var message = new String(template)
                .replaceAll("%\\{consoleUrl}", dashboardUri.getHost())
                .replaceAll("%\\{email}", pendingMailVerification.getMailAddress())
                .replaceAll("%\\{code}", pendingMailVerification.getVerificationCode());
        return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message);
    }

}