From 4042dabd35fdd1fab908efaa38b555fc1d58633b Mon Sep 17 00:00:00 2001 From: Michael Simon <simon@kit.edu> Date: Fri, 21 Jun 2024 11:43:52 +0200 Subject: [PATCH] ISSUE-200 add emails from identity attribute sources to identity --- .../entity/identity/EmailAddressStatus.java | 4 +- .../identity/IdentityEmailAddressService.java | 136 +------------ .../scc/webreg/bean/EmailAddressesBean.java | 43 +++- .../IdentityEmailAddressConverter.java | 32 +++ .../main/resources/msg/messages_de.properties | 20 +- .../main/resources/msg/messages_en.properties | 20 +- .../main/resources/msg/messages_fr.properties | 20 +- .../main/webapp/user/email-addresses.xhtml | 80 ++++++-- .../src/main/webapp/user/ssh-keys.xhtml | 23 ++- regapp-idty/pom.xml | 5 + .../proc/EmailMergeValueProcessor.java | 36 ++++ .../proc/EmailToIdentityValueProcessor.java | 81 ++++++++ .../proc/IdentityValuesProcessor.java | 4 +- .../proc/SingleStringMergeValueProcessor.java | 4 +- .../identity/IdentityEmailAddressHandler.java | 188 ++++++++++++++++++ 15 files changed, 525 insertions(+), 171 deletions(-) create mode 100644 bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java diff --git a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java index 1d3d0a14f..e1418770a 100644 --- a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java +++ b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java @@ -3,5 +3,7 @@ package edu.kit.scc.webreg.entity.identity; public enum EmailAddressStatus { VERIFIED, - UNVERIFIED + UNVERIFIED, + FROM_ATTRIBUTE_VERIFIED, + FROM_ATTRIBUTE_UNVERIFIED, } diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java index c398171f5..7230c21b3 100644 --- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java +++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java @@ -1,170 +1,54 @@ package edu.kit.scc.webreg.service.identity; import java.io.Serializable; -import java.math.BigInteger; -import java.security.SecureRandom; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; - -import edu.kit.scc.regapp.mail.api.TemplateMailService; -import edu.kit.scc.webreg.bootstrap.ApplicationConfig; import edu.kit.scc.webreg.dao.BaseDao; import edu.kit.scc.webreg.dao.identity.IdentityEmailAddressDao; -import edu.kit.scc.webreg.dao.ops.RqlExpressions; -import edu.kit.scc.webreg.entity.EventType; -import edu.kit.scc.webreg.entity.identity.EmailAddressStatus; import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; -import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity_; import edu.kit.scc.webreg.entity.identity.IdentityEntity; -import edu.kit.scc.webreg.event.EventSubmitter; -import edu.kit.scc.webreg.event.IdentityEmailAddressEvent; -import edu.kit.scc.webreg.event.exc.EventSubmitException; import edu.kit.scc.webreg.exc.VerificationException; import edu.kit.scc.webreg.service.impl.BaseServiceImpl; import jakarta.ejb.Stateless; import jakarta.inject.Inject; import jakarta.mail.internet.AddressException; -import jakarta.mail.internet.InternetAddress; @Stateless public class IdentityEmailAddressService extends BaseServiceImpl<IdentityEmailAddressEntity> implements Serializable { private static final long serialVersionUID = 1L; - @Inject - private Logger logger; - @Inject private IdentityEmailAddressDao dao; @Inject - private ApplicationConfig appConfig; - - @Inject - private TemplateMailService mailService; - - @Inject - private EventSubmitter eventSubmitter; + private IdentityEmailAddressHandler handler; public IdentityEmailAddressEntity addEmailAddress(IdentityEntity identity, String emailAddress, String executor) throws AddressException { - InternetAddress email = new InternetAddress(emailAddress, true); - - IdentityEmailAddressEntity entity = dao.createNew(); - entity.setIdentity(identity); - entity.setEmailAddress(email.getAddress()); - entity.setVerificationToken(generateToken()); - entity.setTokenValidUntil(generateTokenValidity()); - entity.setEmailStatus(EmailAddressStatus.UNVERIFIED); - entity = dao.persist(entity); - sendVerificationEmail(entity); - - IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); - try { - eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_ADDED, executor); - } catch (EventSubmitException e) { - logger.warn("Could not submit event", e); - } - - return entity; + return handler.addEmailAddress(identity, emailAddress, executor); } public void redoVerification(IdentityEmailAddressEntity entity, String executor) { entity = dao.fetch(entity.getId()); - entity.setVerificationToken(generateToken()); - entity.setTokenValidUntil(generateTokenValidity()); - - sendVerificationEmail(entity); - - IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); - try { - eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_REDO_VERIFICATION, executor); - } catch (EventSubmitException e) { - logger.warn("Could not submit event", e); - } + handler.redoVerification(entity, executor); } public void deleteEmailAddress(IdentityEmailAddressEntity entity, String executor) { entity = dao.fetch(entity.getId()); - if (entity.equals(entity.getIdentity().getPrimaryEmail())) { - entity.getIdentity().setPrimaryEmail(null); - } - dao.delete(entity); + handler.deleteEmailAddress(entity, executor); } - public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor) - throws VerificationException { - IdentityEmailAddressEntity entity = dao - .find(RqlExpressions.equal(IdentityEmailAddressEntity_.verificationToken, token)); - - if (entity == null) { - throw new VerificationException("no_token_found"); - } - - if (!identity.equals(entity.getIdentity())) { - throw new VerificationException("not_owner"); - } - - if (entity.getTokenValidUntil().before(new Date())) { - throw new VerificationException("token_expired"); - } - - entity.setVerificationToken(null); - entity.setVerifiedOn(new Date()); - entity.setValidUntil(generateValidity()); - entity.setEmailStatus(EmailAddressStatus.VERIFIED); - - IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); - try { - eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_VERIFIED, executor); - } catch (EventSubmitException e) { - logger.warn("Could not submit event", e); - } - return entity; - } - - private String generateToken() { - SecureRandom random = new SecureRandom(); - String t = new BigInteger(130, random).toString(32); - return t; - } - - private Date generateTokenValidity() { - Long validity = 30 * 60 * 1000L; - if (appConfig.getConfigValue("email_token_validity") != null) { - validity = Long.parseLong(appConfig.getConfigValue("email_token_validity")); - } - - return new Date(System.currentTimeMillis() + validity); - } - - private Date generateValidity() { - Long validity = 180 * 24 * 60 * 60 * 1000L; - if (appConfig.getConfigValue("email_validity") != null) { - validity = Long.parseLong(appConfig.getConfigValue("email_validity")); - } - - return new Date(System.currentTimeMillis() + validity); + public void setPrimaryEmailAddress(IdentityEmailAddressEntity entity, String executor) { + entity = dao.fetch(entity.getId()); + handler.setPrimaryEmailAddress(entity, executor); } - protected void sendVerificationEmail(IdentityEmailAddressEntity emailAddress) { - - logger.debug("Sending Email verification mail for identity {} (email: {})", emailAddress.getIdentity().getId(), - emailAddress.getEmailAddress()); - - String templateName = appConfig.getConfigValueOrDefault("email_verification_template", "email_verification"); - - Map<String, Object> context = new HashMap<String, Object>(3); - context.put("emailAddress", emailAddress); - context.put("identity", emailAddress.getIdentity()); + public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor) + throws VerificationException { - mailService.sendMail(templateName, context, true); - emailAddress.setVerificationSent(new Date()); + return handler.checkVerification(identity, token, executor); } @Override diff --git a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java index a08ad67e3..3e7d1dff3 100644 --- a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java +++ b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java @@ -11,7 +11,10 @@ package edu.kit.scc.webreg.bean; import java.io.Serializable; +import java.util.Comparator; +import java.util.List; +import edu.kit.scc.webreg.entity.identity.EmailAddressStatus; import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; import edu.kit.scc.webreg.entity.identity.IdentityEntity; import edu.kit.scc.webreg.entity.identity.IdentityEntity_; @@ -51,8 +54,13 @@ public class EmailAddressesBean implements Serializable { private String addEmailAddress; private String token; - + private List<IdentityEmailAddressEntity> primaryEmailList; + private IdentityEmailAddressEntity chosenPrimary; + public void preRenderView(ComponentSystemEvent ev) { + if (getToken() != null) { + checkVerification(); + } } public void addEmailAddress() { @@ -69,17 +77,27 @@ public class EmailAddressesBean implements Serializable { service.deleteEmailAddress(address, "idty-" + session.getIdentityId()); identity = null; } - + + public void setPrimaryEmailAddress() { + service.setPrimaryEmailAddress(getChosenPrimary(), "idty-" + session.getIdentityId()); + identity = null; + chosenPrimary = null; + messageGenerator.addResolvedInfoMessage("email_addresses.set_primary_email_success", + "email_addresses.set_primary_email_success_detail", true); + } + public void checkVerification() { try { service.checkVerification(getIdentity(), getToken(), "idty-" + session.getIdentityId()); token = null; identity = null; + messageGenerator.addResolvedInfoMessage("email_addresses.verification_success", + "email_addresses.verification_success_detail", true); } catch (VerificationException e) { messageGenerator.addResolvedErrorMessage("email_addresses." + e.getMessage()); } } - + public IdentityEntity getIdentity() { if (identity == null) { identity = identityService.findByIdWithAttrs(session.getIdentityId(), IdentityEntity_.emailAddresses); @@ -102,4 +120,23 @@ public class EmailAddressesBean implements Serializable { public void setToken(String token) { this.token = token; } + + public List<IdentityEmailAddressEntity> getPrimaryEmailList() { + if (primaryEmailList == null) { + primaryEmailList = getIdentity().getEmailAddresses().stream() + .filter(e -> !EmailAddressStatus.UNVERIFIED.equals(e.getEmailStatus())).toList(); + } + return primaryEmailList; + } + + public IdentityEmailAddressEntity getChosenPrimary() { + if (chosenPrimary == null) { + chosenPrimary = getIdentity().getPrimaryEmail(); + } + return chosenPrimary; + } + + public void setChosenPrimary(IdentityEmailAddressEntity chosenPrimary) { + this.chosenPrimary = chosenPrimary; + } } diff --git a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java new file mode 100644 index 000000000..010895ed1 --- /dev/null +++ b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2014 Michael Simon. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Michael Simon - initial + ******************************************************************************/ +package edu.kit.scc.webreg.converter; + +import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; +import edu.kit.scc.webreg.service.BaseService; +import edu.kit.scc.webreg.service.identity.IdentityEmailAddressService; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +@Named("identityEmailAddressConverter") +public class IdentityEmailAddressConverter extends AbstractConverter<IdentityEmailAddressEntity> { + + private static final long serialVersionUID = 1L; + + @Inject + private IdentityEmailAddressService service; + + @Override + protected BaseService<IdentityEmailAddressEntity> getService() { + return service; + } + +} diff --git a/bwreg-webapp/src/main/resources/msg/messages_de.properties b/bwreg-webapp/src/main/resources/msg/messages_de.properties index 8740280e9..d2dc7e688 100644 --- a/bwreg-webapp/src/main/resources/msg/messages_de.properties +++ b/bwreg-webapp/src/main/resources/msg/messages_de.properties @@ -373,9 +373,23 @@ email_address = E-Mail-Adresse email_address_more = E-Mail-Adresse (weitere) -email_addresses.heading = Meine E-Mail Adressen -email_addresses.no_primary_email_set = Keine gesetzt -email_addresses.primary_email = Prim\u00E4re E-Mail Adresse +email_addresses.add_email_address = Neue E-Mail Adresse hinzuf\u00FCgen +email_addresses.from_attribute_source = E-Mail Adresse von einer externen Quelle +email_addresses.heading = Meine E-Mail Adressen +email_addresses.no_primary_email_set = Keine gesetzt +email_addresses.no_token_found = Dieses Token ist unbekannt +email_addresses.not_owner = Falsches Token +email_addresses.primary_email = Prim\u00E4re E-Mail Adresse +email_addresses.token_expired = Das Token ist abgelaufen, bitte lassen Sie sich ein neues zusenden. +email_addresses.token_sent = \u00DCberpr\u00FCfungstoken gesendet +email_addresses.token_valid_until = Token g\u00FCltig bis +email_addresses.unverified = Noch nicht verifiziert +email_addresses.verification_success = Verifizierung erfolgreich +email_addresses.verification_success_detail = Die \u00DCberpr\u00FCfung der E-Mail-Adresse war erfolgreich. Sie k\u00F6nnen sie nun verwenden. +email_addresses.verification_token = E-Mail Adresse best\u00E4tigen +email_addresses.verification_valid_until = G\u00FCltig bis +email_addresses.verified = Verifiziert +email_addresses.verified_on = Verifiziert am email_signature_keys = Schl\u00FCssel zum Signieren diff --git a/bwreg-webapp/src/main/resources/msg/messages_en.properties b/bwreg-webapp/src/main/resources/msg/messages_en.properties index 9d67eca36..298dc2c23 100644 --- a/bwreg-webapp/src/main/resources/msg/messages_en.properties +++ b/bwreg-webapp/src/main/resources/msg/messages_en.properties @@ -373,9 +373,23 @@ email_address = E-Mail address email_address_more = E-Mail address (more) -email_addresses.heading = My E-Mail-Addresses -email_addresses.no_primary_email_set = None set -email_addresses.primary_email = Primary e-mail address +email_addresses.add_email_address = Add new e-mail address +email_addresses.from_attribute_source = E-mail address from an external source +email_addresses.heading = My E-Mail-Addresses +email_addresses.no_primary_email_set = None set +email_addresses.no_token_found = This token is unknown +email_addresses.not_owner = Token wrong +email_addresses.primary_email = Primary e-mail address +email_addresses.token_expired = The token has expired, please have a new one sent to you. +email_addresses.token_sent = Verification token sent +email_addresses.token_valid_until = Token valid until +email_addresses.unverified = Not yet verified +email_addresses.verification_success = Verification successful +email_addresses.verification_success_detail = The verification of the e-mail address was successful. You can now use it. +email_addresses.verification_token = Confirm e-mail address +email_addresses.verification_valid_until = Valid until +email_addresses.verified = Verified +email_addresses.verified_on = Verified on email_signature_keys = Keys for signing emails diff --git a/bwreg-webapp/src/main/resources/msg/messages_fr.properties b/bwreg-webapp/src/main/resources/msg/messages_fr.properties index 8f3f60827..2ff5a7874 100644 --- a/bwreg-webapp/src/main/resources/msg/messages_fr.properties +++ b/bwreg-webapp/src/main/resources/msg/messages_fr.properties @@ -373,9 +373,23 @@ email_address = Adresse \u00E9lectronique email_address_more = Adresse \u00E9lectronique (plus) -email_addresses.heading = Mes adresses e-mail -email_addresses.no_primary_email_set = Aucune d\u00E9finie -email_addresses.primary_email = Adresse e-mail primaire +email_addresses.add_email_address = Ajouter une nouvelle adresse e-mail +email_addresses.from_attribute_source = Adresse e-mail d'une source externe +email_addresses.heading = Mes adresses e-mail +email_addresses.no_primary_email_set = Aucune d\u00E9finie +email_addresses.no_token_found = Ce token est inconnu +email_addresses.not_owner = Mauvais token +email_addresses.primary_email = Adresse e-mail primaire +email_addresses.token_expired = Le token est expir\u00E9, veuillez vous en faire envoyer un nouveau. +email_addresses.token_sent = Jeton de v\u00E9rification envoy\u00E9 +email_addresses.token_valid_until = Token valable jusqu'au +email_addresses.unverified = Pas encore v\u00E9rifi\u00E9 +email_addresses.verification_success = V\u00E9rification r\u00E9ussie +email_addresses.verification_success_detail = La v\u00E9rification de l'adresse e-mail a \u00E9t\u00E9 effectu\u00E9e avec succ\u00E8s. Vous pouvez maintenant l'utiliser. +email_addresses.verification_token = Confirmer l'adresse e-mail +email_addresses.verification_valid_until = Valable jusqu'au +email_addresses.verified = V\u00E9rifi\u00E9 +email_addresses.verified_on = V\u00E9rifi\u00E9 le email_signature_keys = Cl\u00E9s pour signer des e-mails diff --git a/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml b/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml index 22b12b3ea..463f4cc3f 100644 --- a/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml +++ b/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml @@ -14,10 +14,11 @@ <body> <f:view> -<f:metadata> - <f:event type="jakarta.faces.event.PreRenderViewEvent" - listener="#{emailAddressesBean.preRenderView}" /> -</f:metadata> + <f:metadata> + <f:viewParam name="t" value="#{emailAddressesBean.token}"/> + <f:event type="jakarta.faces.event.PreRenderViewEvent" + listener="#{emailAddressesBean.preRenderView}" /> + </f:metadata> <ui:composition template="/template/default.xhtml"> <ui:param name="title" value="#{messages.title}"/> @@ -29,25 +30,70 @@ <div><p:messages showDetail="true" /></div> + <div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9"> + <p:selectOneListbox id="userSelect" var="e" value="#{emailAddressesBean.chosenPrimary}" converter="#{identityEmailAddressConverter}" + style="font-size: 1em;"> + <f:selectItems value="#{emailAddressesBean.primaryEmailList}" var="email" itemLabel="#{email.emailAddress}" itemValue="#{email}" /> + <p:column> + <h:outputText value="#{e.emailAddress}" /> + </p:column> + </p:selectOneListbox> + <div class="form"> + <p:commandButton action="#{emailAddressesBean.setPrimaryEmailAddress()}" value="#{messages['set']}" validateClient="true" ajax="true" update="@all" /> + </div> + </div> <div> <h:outputText value="#{messages['email_addresses.primary_email']}: "/> - <h:outputText value="#{emailAddressesBean.identity.primaryEmail}" rendered="#{emailAddressesBean.identity.primaryEmail != null}"/> + <h:outputText value="#{emailAddressesBean.identity.primaryEmail.emailAddress}" rendered="#{emailAddressesBean.identity.primaryEmail != null}"/> <h:outputText value="#{messages['email_addresses.no_primary_email_set']}" rendered="#{emailAddressesBean.identity.primaryEmail == null}"/> </div> - + <ui:repeat var="email" value="#{emailAddressesBean.identity.emailAddresses}"> - <div> - <h:outputText value="#{email.emailAddress}" />, - <h:outputText value="#{email.emailStatus}" />, - <h:outputText value="#{email.tokenValidUntil}" />, - <h:outputText value="#{email.verificationSent}" />, - <h:outputText value="#{email.verifiedOn}" />, - <h:outputText value="#{email.validUntil}" />, - <p:commandLink action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" update="@all"/> - </div> + <h:panelGroup layout="block" styleClass="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9" + style="margin: 1em; background-color: #f8f9fa;border: 1px solid #dee2e6;"> + <div> + <b><h:outputText value="#{email.emailAddress}"/></b> + </div> + <h:panelGroup rendered="#{email.emailStatus == 'FROM_ATTRIBUTE_UNVERIFIED'}"> + <h:outputText value="#{messages['email_addresses.from_attribute_source']}"/> + </h:panelGroup> + <h:panelGroup rendered="#{email.emailStatus == 'FROM_ATTRIBUTE_VERIFIED'}"> + <h:outputText value="#{messages['email_addresses.from_attribute_source']}"/> + </h:panelGroup> + <h:panelGroup rendered="#{email.emailStatus == 'UNVERIFIED'}"> + <div> + <h:outputText value="#{messages['email_addresses.unverified']}"/> + </div> + <div> + <h:outputText value="#{messages['email_addresses.token_sent']} "/><h:outputText value="#{of:formatDate(email.verificationSent, 'dd.MM.yyyy HH:mm')}"/> + </div> + <div> + <h:outputText value="#{messages['email_addresses.token_valid_until']}"/> #{of:formatDate(email.tokenValidUntil, 'dd.MM.yyyy HH:mm')} + </div> + + <div class="form"> + <p:commandButton action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" ajax="true" update="@all" /> + </div> + </h:panelGroup> + <h:panelGroup rendered="#{email.emailStatus == 'VERIFIED'}"> + <div> + <h:outputText value="#{messages['email_addresses.verified']}"/> + </div> + <div> + <h:outputText value="#{messages['email_addresses.verified_on']} "/><h:outputText value="#{of:formatDate(email.verifiedOn, 'dd.MM.yyyy HH:mm')}"/> + </div> + <div> + <h:outputText value="#{messages['email_addresses.verification_valid_until']} "/> <h:outputText value="#{of:formatDate(email.validUntil, 'dd.MM.yyyy HH:mm')}"/> + </div> + + <div class="form"> + <p:commandButton action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" ajax="true" update="@all" /> + </div> + </h:panelGroup> + </h:panelGroup> </ui:repeat> - <div> + <div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9"> <h:outputText value="#{messages['email_addresses.add_email_address']}: "/> <p:inputText type="email" value="#{emailAddressesBean.addEmailAddress}" /> <div class="form"> @@ -55,7 +101,7 @@ </div> </div> - <div> + <div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9"> <h:outputText value="#{messages['email_addresses.verification_token']}: "/> <p:inputText value="#{emailAddressesBean.token}" /> <div class="form"> diff --git a/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml b/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml index 4a3eb120c..18d434b89 100644 --- a/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml +++ b/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml @@ -23,7 +23,7 @@ <ui:param name="title" value="#{messages.title}"/> <ui:define name="content"> - <h:form id="form" class="form full fancy"> + <h:form id="form" class="full"> <h3>#{messages['ssh_keys.key_list']}</h3> @@ -33,7 +33,7 @@ columns="2" layout="grid" style="margin-bottom: 16px;"> <p:panel styleClass="grayback" style="margin-bottom: 0px;"> <f:facet name="header"> - <i class="fa fa-fw fa-key"></i> + <i class="pi pi-key"></i> <b><h:outputText value="#{key.pubKeyEntity.name}"/></b> </f:facet> @@ -70,18 +70,19 @@ </h:panelGrid> - <p:commandButton action="#{userSshKeyManagementBean.deleteKey(key.pubKeyEntity.name)}" value="#{messages.revoke}" - update="@form" immediate="true"> - <p:confirm header="#{messages.confirm_header}" message="#{messages.ssh_key_confirm}" /> - </p:commandButton> - + <div class="form"> + <p:commandButton action="#{userSshKeyManagementBean.deleteKey(key.pubKeyEntity.name)}" value="#{messages.revoke}" + update="@form" immediate="true" style="font-size: 1rem;"> + <p:confirm header="#{messages.confirm_header}" message="#{messages.ssh_key_confirm}" /> + </p:commandButton> + </div> </p:panel> </p:dataGrid> - <p:panel> - <p:commandButton id="openAddSshKeyDlg" oncomplete="PF('addSshKeyDlg').show();" value="#{messages.add_ssh_pub_key}"></p:commandButton> - </p:panel> - + <div class="form"> + <p:commandButton id="openAddSshKeyDlg" oncomplete="PF('addSshKeyDlg').show();" value="#{messages.add_ssh_pub_key}" style="font-size: 1rem;"></p:commandButton> + </div> + <p:dialog header="#{messages.add_ssh_pub_key}" widgetVar="addSshKeyDlg" id="addSshKeyDlgId" modal="true" closable="true" closeOnEscape="true" showEffect="fade" hideEffect="fade"> diff --git a/regapp-idty/pom.xml b/regapp-idty/pom.xml index f886842e3..681b88143 100644 --- a/regapp-idty/pom.xml +++ b/regapp-idty/pom.xml @@ -91,6 +91,11 @@ <artifactId>regapp-as</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>edu.kit.scc</groupId> + <artifactId>regapp-mail</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>jakarta.platform</groupId> diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java new file mode 100644 index 000000000..645538d9b --- /dev/null +++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java @@ -0,0 +1,36 @@ +package edu.kit.scc.webreg.service.attribute.proc; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import edu.kit.scc.webreg.entity.attribute.IdentityAttributeSetEntity; +import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; +import edu.kit.scc.webreg.entity.identity.IdentityEntity; +import edu.kit.scc.webreg.service.identity.IdentityAttributeResolver; +import jakarta.enterprise.inject.spi.CDI; + +public class EmailMergeValueProcessor extends SingleStringMergeValueProcessor { + + public EmailMergeValueProcessor(String outputAttribute, String... inspectValues) { + super(outputAttribute, inspectValues); + } + + public void apply(IdentityAttributeSetEntity attributeSet) { + super.apply(attributeSet); + + IdentityEntity identity = attributeSet.getIdentity(); + if (identity.getPrimaryEmail() == null) { + String email = getIdentityAttributeResolver().resolveSingleStringValue(identity, outputAttribute); + Map<String, IdentityEmailAddressEntity> emailMap = identity.getEmailAddresses().stream() + .collect(Collectors.toMap(IdentityEmailAddressEntity::getEmailAddress, Function.identity())); + if (emailMap.containsKey(email)) { + identity.setPrimaryEmail(emailMap.get(email)); + } + } + } + + private IdentityAttributeResolver getIdentityAttributeResolver() { + return CDI.current().select(IdentityAttributeResolver.class).get(); + } +} diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java new file mode 100644 index 000000000..fc3c91beb --- /dev/null +++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java @@ -0,0 +1,81 @@ +package edu.kit.scc.webreg.service.attribute.proc; + +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import edu.kit.scc.webreg.entity.attribute.IdentityAttributeSetEntity; +import edu.kit.scc.webreg.entity.attribute.value.StringListValueEntity; +import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity; +import edu.kit.scc.webreg.entity.attribute.value.ValueEntity; +import edu.kit.scc.webreg.entity.identity.EmailAddressStatus; +import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; +import edu.kit.scc.webreg.entity.identity.IdentityEntity; +import edu.kit.scc.webreg.service.identity.IdentityEmailAddressHandler; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.mail.internet.AddressException; + +public class EmailToIdentityValueProcessor extends StringListMergeValueProcessor { + + public EmailToIdentityValueProcessor(String outputAttribute, String... inspectValues) { + super(outputAttribute, inspectValues); + } + + public void apply(IdentityAttributeSetEntity attributeSet) { + super.apply(attributeSet); + + Set<String> emailAddresses = new HashSet<>(); + for (ValueEntity value : getValueList()) { + if (value instanceof StringValueEntity) { + emailAddresses.add(((StringValueEntity) value).getValueString()); + } else if (value instanceof StringListValueEntity) { + if (((StringListValueEntity) value).getValueList() != null) { + emailAddresses.addAll(((StringListValueEntity) value).getValueList()); + } + } + } + + IdentityEntity identity = attributeSet.getIdentity(); + Map<String, IdentityEmailAddressEntity> emailMap = identity.getEmailAddresses().stream() + .collect(Collectors.toMap(IdentityEmailAddressEntity::getEmailAddress, Function.identity())); + + for (String email : emailAddresses) { + // Add email addresses from attribute sources to identity + if (! emailMap.containsKey(email)) { + createIdentityEmailAddress(identity, email); + } + } + for (Entry<String, IdentityEmailAddressEntity> entry : emailMap.entrySet()) { + // Remove email addresses which are attribute types and no longer there + if (EmailAddressStatus.FROM_ATTRIBUTE_UNVERIFIED.equals(entry.getValue().getEmailStatus()) || + EmailAddressStatus.FROM_ATTRIBUTE_VERIFIED.equals(entry.getValue().getEmailStatus())) { + if (! emailAddresses.contains(entry.getValue().getEmailAddress())) { + deleteIdentityEmailAddress(identity, entry.getValue()); + } + } + } + } + + private void deleteIdentityEmailAddress(IdentityEntity identity, IdentityEmailAddressEntity email) { + IdentityEmailAddressHandler handler = getIdentityEmailAddressHandler(); + if (identity.getPrimaryEmail().equals(email)) + identity.setPrimaryEmail(null); + handler.deleteEmailAddress(email, "idty-" + identity.getId()); + } + + private void createIdentityEmailAddress(IdentityEntity identity, String email) { + IdentityEmailAddressHandler handler = getIdentityEmailAddressHandler(); + try { + handler.addEmailAddressFromAttribute(identity, email, "idty-" + identity.getId()); + } catch (AddressException e) { + logger.info("Unparsable email address: {} error: {}", email, e.getMessage()); + } + } + + private IdentityEmailAddressHandler getIdentityEmailAddressHandler() { + return CDI.current().select(IdentityEmailAddressHandler.class).get(); + } +} diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java index 937e5bc4f..e95628c88 100644 --- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java +++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java @@ -24,8 +24,8 @@ public class IdentityValuesProcessor { } private List<ValueProcessor> loadProcessors() { - return Arrays.asList(new StringListMergeValueProcessor("email_all", "email"), - new SingleStringMergeValueProcessor("email", "email"), + return Arrays.asList(new EmailToIdentityValueProcessor("email_all", "email"), + new EmailMergeValueProcessor("email", "email"), new StringListMergeValueProcessor("voperson_external_affiliation", "eduperson_affiliation"), new StringListMergeValueProcessor("eduperson_assurance", "eduperson_assurance"), new StringListMergeAuthorityValueProcessor("eduperson_entitlement", "eduperson_entitlement"), diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java index 22f331a94..36cb36379 100644 --- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java +++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java @@ -17,8 +17,8 @@ import edu.kit.scc.webreg.entity.attribute.value.ValueEntity; public class SingleStringMergeValueProcessor extends AbstractListProcessor { - private String outputAttribute; - private String[] inspectValues; + protected String outputAttribute; + protected String[] inspectValues; public SingleStringMergeValueProcessor(String outputAttribute, String... inspectValues) { this.outputAttribute = outputAttribute; diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java new file mode 100644 index 000000000..e4f1b1648 --- /dev/null +++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java @@ -0,0 +1,188 @@ +package edu.kit.scc.webreg.service.identity; + +import java.io.Serializable; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; + +import edu.kit.scc.regapp.mail.impl.TemplateMailSender; +import edu.kit.scc.webreg.bootstrap.ApplicationConfig; +import edu.kit.scc.webreg.dao.identity.IdentityEmailAddressDao; +import edu.kit.scc.webreg.dao.ops.RqlExpressions; +import edu.kit.scc.webreg.entity.EventType; +import edu.kit.scc.webreg.entity.identity.EmailAddressStatus; +import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity; +import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity_; +import edu.kit.scc.webreg.entity.identity.IdentityEntity; +import edu.kit.scc.webreg.event.EventSubmitter; +import edu.kit.scc.webreg.event.IdentityEmailAddressEvent; +import edu.kit.scc.webreg.event.exc.EventSubmitException; +import edu.kit.scc.webreg.exc.VerificationException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; + +@ApplicationScoped +public class IdentityEmailAddressHandler implements Serializable { + + private static final long serialVersionUID = 1L; + + @Inject + private Logger logger; + + @Inject + private IdentityEmailAddressDao dao; + + @Inject + private ApplicationConfig appConfig; + + @Inject + private TemplateMailSender mailService; + + @Inject + private EventSubmitter eventSubmitter; + + public void setPrimaryEmailAddress(IdentityEmailAddressEntity entity, String executor) { + entity.getIdentity().setPrimaryEmail(entity); + } + + public IdentityEmailAddressEntity addEmailAddressFromAttribute(IdentityEntity identity, String emailAddress, String executor) + throws AddressException { + InternetAddress email = new InternetAddress(emailAddress, true); + + IdentityEmailAddressEntity entity = dao.createNew(); + entity.setIdentity(identity); + entity.setEmailAddress(email.getAddress()); + entity.setEmailStatus(EmailAddressStatus.FROM_ATTRIBUTE_UNVERIFIED); + entity = dao.persist(entity); + + return entity; + + } + + public IdentityEmailAddressEntity addEmailAddress(IdentityEntity identity, String emailAddress, String executor) + throws AddressException { + InternetAddress email = new InternetAddress(emailAddress, true); + + IdentityEmailAddressEntity entity = dao.createNew(); + entity.setIdentity(identity); + entity.setEmailAddress(email.getAddress()); + entity.setVerificationToken(generateToken()); + entity.setTokenValidUntil(generateTokenValidity()); + entity.setEmailStatus(EmailAddressStatus.UNVERIFIED); + entity = dao.persist(entity); + + sendVerificationEmail(entity); + + IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); + try { + eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_ADDED, executor); + } catch (EventSubmitException e) { + logger.warn("Could not submit event", e); + } + + return entity; + } + + public void redoVerification(IdentityEmailAddressEntity entity, String executor) { + entity.setVerificationToken(generateToken()); + entity.setTokenValidUntil(generateTokenValidity()); + + sendVerificationEmail(entity); + + IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); + try { + eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_REDO_VERIFICATION, executor); + } catch (EventSubmitException e) { + logger.warn("Could not submit event", e); + } + } + + public void deleteEmailAddress(IdentityEmailAddressEntity entity, String executor) { + if (entity.equals(entity.getIdentity().getPrimaryEmail())) { + entity.getIdentity().setPrimaryEmail(null); + } + dao.delete(entity); + } + + public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor) + throws VerificationException { + IdentityEmailAddressEntity entity = dao + .find(RqlExpressions.equal(IdentityEmailAddressEntity_.verificationToken, token)); + + if (entity == null) { + throw new VerificationException("no_token_found"); + } + + if (!identity.equals(entity.getIdentity())) { + throw new VerificationException("not_owner"); + } + + if (entity.getTokenValidUntil().before(new Date())) { + throw new VerificationException("token_expired"); + } + + entity.setVerificationToken(null); + entity.setVerifiedOn(new Date()); + entity.setValidUntil(generateValidity()); + entity.setEmailStatus(EmailAddressStatus.VERIFIED); + + if (identity.getPrimaryEmail() == null) { + // use identity from jpa session, the object from method call is detached + identity = entity.getIdentity(); + identity.setPrimaryEmail(entity); + } + + IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity); + try { + eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_VERIFIED, executor); + } catch (EventSubmitException e) { + logger.warn("Could not submit event", e); + } + return entity; + } + + private String generateToken() { + SecureRandom random = new SecureRandom(); + String t = new BigInteger(130, random).toString(32); + return t; + } + + private Date generateTokenValidity() { + Long validity = 30 * 60 * 1000L; + if (appConfig.getConfigValue("email_token_validity") != null) { + validity = Long.parseLong(appConfig.getConfigValue("email_token_validity")); + } + + return new Date(System.currentTimeMillis() + validity); + } + + private Date generateValidity() { + Long validity = 180 * 24 * 60 * 60 * 1000L; + if (appConfig.getConfigValue("email_validity") != null) { + validity = Long.parseLong(appConfig.getConfigValue("email_validity")); + } + + return new Date(System.currentTimeMillis() + validity); + } + + protected void sendVerificationEmail(IdentityEmailAddressEntity emailAddress) { + + logger.debug("Sending Email verification mail for identity {} (email: {})", emailAddress.getIdentity().getId(), + emailAddress.getEmailAddress()); + + String templateName = appConfig.getConfigValueOrDefault("email_verification_template", "email_verification"); + + Map<String, Object> context = new HashMap<String, Object>(3); + context.put("emailAddress", emailAddress); + context.put("identity", emailAddress.getIdentity()); + + mailService.sendMail(templateName, context, true); + emailAddress.setVerificationSent(new Date()); + } +} -- GitLab