Skip to content

Commit

Permalink
send notification for put role membership decision (#2737)
Browse files Browse the repository at this point in the history
* send notification for put role membership decision

Signed-off-by: craman <[email protected]>

* update test

Signed-off-by: craman <[email protected]>

---------

Signed-off-by: craman <[email protected]>
Co-authored-by: craman <[email protected]>
  • Loading branch information
chandrasekhar1996 and craman authored Sep 26, 2024
1 parent d0dd183 commit 515c5fa
Show file tree
Hide file tree
Showing 17 changed files with 1,396 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public enum Type {
PENDING_ROLE_APPROVAL,
PENDING_GROUP_APPROVAL,
CERT_FAILED_REFRESH,
AWS_ZTS_HEALTH
AWS_ZTS_HEALTH,
ROLE_MEMBER_DECISION,
GROUP_MEMBER_DECISION
}

// type of the notification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
public final class NotificationServiceConstants {
public static final String NOTIFICATION_PROP_SERVICE_FACTORY_CLASS = "athenz.zms.notification_service_factory_class";

public static final String NOTIFICATION_DETAILS_DOMAIN = "domain";
public static final String NOTIFICATION_DETAILS_ROLE = "role";
public static final String NOTIFICATION_DETAILS_GROUP = "group";
public static final String NOTIFICATION_DETAILS_MEMBER = "member";
public static final String NOTIFICATION_DETAILS_REASON = "reason";
public static final String NOTIFICATION_DETAILS_REQUESTER = "requester";
public static final String NOTIFICATION_DETAILS_ROLES_LIST = "rolesList";
public static final String NOTIFICATION_DETAILS_MEMBERS_LIST = "membersList";
public static final String NOTIFICATION_DETAILS_UNREFRESHED_CERTS = "unrefreshedCerts";
public static final String NOTIFICATION_DETAILS_AWS_ZTS_HEALTH = "awsZtsHealth";
public static final String NOTIFICATION_DETAILS_AFFECTED_ZTS = "affectedZts";
public static final String NOTIFICATION_DETAILS_DOMAIN = "domain";
public static final String NOTIFICATION_DETAILS_ROLE = "role";
public static final String NOTIFICATION_DETAILS_GROUP = "group";
public static final String NOTIFICATION_DETAILS_MEMBER = "member";
public static final String NOTIFICATION_DETAILS_REASON = "reason";
public static final String NOTIFICATION_DETAILS_REQUESTER = "requester";
public static final String NOTIFICATION_DETAILS_ROLES_LIST = "rolesList";
public static final String NOTIFICATION_DETAILS_MEMBERS_LIST = "membersList";
public static final String NOTIFICATION_DETAILS_UNREFRESHED_CERTS = "unrefreshedCerts";
public static final String NOTIFICATION_DETAILS_AWS_ZTS_HEALTH = "awsZtsHealth";
public static final String NOTIFICATION_DETAILS_AFFECTED_ZTS = "affectedZts";
public static final String NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION_PRINCIPAL = "actionPrincipal";
public static final String NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_STATE = "pendingState";
public static final String NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION = "membershipDecision";

public static final String HTML_LOGO_CID_PLACEHOLDER = "<logo>";
public static final String CHARSET_UTF_8 = "UTF-8";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class MetricNotificationService implements NotificationService {
public static final String METRIC_NOTIFICATION_GROUP_KEY = "group";
public static final String METRIC_NOTIFICATION_REASON_KEY = "reason";
public static final String METRIC_NOTIFICATION_REQUESTER_KEY = "requester";
public static final String METRIC_NOTIFICATION_MEMBERSHIP_DECISION = "membership_decision";

private final Metric metric;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ athenz.notification.email.group_membership.reminder.subject=Group Membership App
athenz.notification.email.domain.group_member.expiry.subject=Athenz Domain Group Member Expiration Notification

athenz.notification.email.group_member.expiry.subject=Athenz Group Member Expiration Notification

athenz.notification.email.pending_role_membership.decision.reject.subject=Athenz Pending Role Member Rejected

athenz.notification.email.pending_role_membership.decision.approval.subject=Athenz Pending Role Member Approved
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
athenz.notification.email.role_member.expiry.subject=Athenz Role Member Expiration Notification
athenz.notification.email.pending_role_membership.decision.reject.subject=Athenz Pending Role Member Rejected
athenz.notification.email.pending_role_membership.decision.approval.subject=Athenz Pending Role Member Approved
15 changes: 15 additions & 0 deletions servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9721,6 +9721,21 @@ public void executePutPrincipalState(ResourceContext ctx, final String domainNam
}
}

RoleMember getPendingRoleMember(String domainName, String roleName, String memberName) {
final String caller = "getPendingRoleMember";
try (ObjectStoreConnection con = store.getConnection(true, false)) {
RoleMember pendingMember = con.getPendingRoleMember(domainName, roleName, memberName);
if (pendingMember == null) {
throw ZMSUtils.notFoundError("Pending role member " + memberName + " not found", caller);
}
return pendingMember;
} catch (ResourceException ex) {
LOG.error("getPendingRoleMember: error getting pending member {} from {}:role.{} - error {}",
memberName, domainName, roleName, ex.getMessage());
throw ex;
}
}

class UserAuthorityFilterEnforcer implements Runnable {

public UserAuthorityFilterEnforcer() {
Expand Down
2 changes: 2 additions & 0 deletions servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ public final class ZMSConsts {
//pending member
public static final String PENDING_REQUEST_ADD_STATE = "ADD";
public static final String PENDING_REQUEST_DELETE_STATE = "DELETE";
public static final String PENDING_REQUEST_APPROVE = "approve";
public static final String PENDING_REQUEST_REJECT = "reject";

public static final String ZMS_PROP_JSON_MAX_NESTING_DEPTH = "athenz.zms.json_max_nesting_depth";
public static final String ZMS_PROP_JSON_MAX_NUMBER_LENGTH = "athenz.zms.json_max_number_length";
Expand Down
34 changes: 34 additions & 0 deletions servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import com.yahoo.athenz.common.utils.SignUtils;
import com.yahoo.athenz.zms.config.*;
import com.yahoo.athenz.zms.notification.PutGroupMembershipNotificationTask;
import com.yahoo.athenz.zms.notification.PutRoleMembershipDecisionNotificationTask;
import com.yahoo.athenz.zms.notification.PutRoleMembershipNotificationTask;
import com.yahoo.athenz.zms.notification.ZMSNotificationTaskFactory;
import com.yahoo.athenz.zms.provider.DomainDependencyProviderResponse;
Expand Down Expand Up @@ -5034,6 +5035,33 @@ void sendGroupMembershipApprovalNotification(final String domain, final String o
notificationManager.sendNotifications(notifications);
}

void sendRoleMembershipDecisionNotification(final String domain, final String roleName,
final RoleMember roleMember, final String auditRef,
final String actionPrincipal, final String pendingState, final String requestPrincipal) {


Map<String, String> details = new HashMap<>();
details.put(NOTIFICATION_DETAILS_DOMAIN, domain);
details.put(NOTIFICATION_DETAILS_ROLE, roleName);
details.put(NOTIFICATION_DETAILS_MEMBER, roleMember.getMemberName());
details.put(NOTIFICATION_DETAILS_REASON, auditRef);
details.put(NOTIFICATION_DETAILS_REQUESTER, requestPrincipal);
details.put(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION_PRINCIPAL, actionPrincipal);
details.put(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_STATE, pendingState);

String membershipDecision = roleMember.getApproved() ? ZMSConsts.PENDING_REQUEST_APPROVE : ZMSConsts.PENDING_REQUEST_REJECT;
details.put(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION, membershipDecision);

if (LOG.isDebugEnabled()) {
LOG.debug("Sending role membership decision notification after putMembershipDecision");
}

List<Notification> notifications = new PutRoleMembershipDecisionNotificationTask(details,
roleMember.getApproved(), dbService, userDomainPrefix,
notificationToEmailConverterCommon).getNotifications();
notificationManager.sendNotifications(notifications);
}

@Override
public void deletePendingMembership(ResourceContext ctx, String domainName, String roleName,
String memberName, String auditRef) {
Expand Down Expand Up @@ -9984,7 +10012,13 @@ public void putMembershipDecision(ResourceContext ctx, String domainName, String
role.getAuditEnabled(), disallowGroups, caller);
}

//get the pending member details to send notification
RoleMember pendingMember = dbService.getPendingRoleMember(domainName, roleName, roleMember.getMemberName());

dbService.executePutMembershipDecision(ctx, domainName, roleName, roleMember, auditRef, caller);

sendRoleMembershipDecisionNotification(domainName, roleName,
roleMember, auditRef, principal.getFullName(), pendingMember.getPendingState(), pendingMember.getRequestPrincipal());
}

private void validatePutMembershipDecisionAuthorization(final Principal principal, final AthenzDomain domain,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.yahoo.athenz.zms.notification;

import com.yahoo.athenz.auth.AuthorityConsts;
import com.yahoo.athenz.auth.util.AthenzUtils;
import com.yahoo.athenz.common.server.notification.*;
import com.yahoo.athenz.zms.DBService;
import com.yahoo.athenz.zms.Group;
import com.yahoo.athenz.zms.utils.ZMSUtils;
import com.yahoo.rdl.Timestamp;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.MessageFormat;
import java.util.*;

import static com.yahoo.athenz.common.ServerCommonConsts.ADMIN_ROLE_NAME;
import static com.yahoo.athenz.common.server.notification.NotificationServiceConstants.*;
import static com.yahoo.athenz.common.server.notification.NotificationServiceConstants.NOTIFICATION_DETAILS_REQUESTER;
import static com.yahoo.athenz.common.server.notification.impl.MetricNotificationService.*;
import static com.yahoo.athenz.common.server.notification.impl.MetricNotificationService.METRIC_NOTIFICATION_REQUESTER_KEY;

public class PutRoleMembershipDecisionNotificationTask implements NotificationTask {

private static final Logger LOGGER = LoggerFactory.getLogger(PutRoleMembershipDecisionNotificationTask.class);

private final Map<String, String> details;
private final NotificationCommon notificationCommon;
private final static String DESCRIPTION = "Pending Membership Decision Notification";
private final PutRoleMembershipDecisionNotificationToEmailConverter putMembershipNotificationToEmailConverter;
private final PutRoleMembershipDecisionNotificationToMetricConverter putMembershipNotificationToMetricConverter;
private final DBService dbService;
private final DomainRoleMembersFetcher domainRoleMembersFetcher;
private final String userDomainPrefix;

public PutRoleMembershipDecisionNotificationTask(Map<String, String> details, Boolean approved, DBService dbService, String userDomainPrefix, NotificationToEmailConverterCommon notificationToEmailConverterCommon) {
this.details = details;
this.userDomainPrefix = userDomainPrefix;
this.domainRoleMembersFetcher = new DomainRoleMembersFetcher(dbService, userDomainPrefix);
this.notificationCommon = new NotificationCommon(domainRoleMembersFetcher, userDomainPrefix);
this.putMembershipNotificationToEmailConverter = new PutRoleMembershipDecisionNotificationToEmailConverter(notificationToEmailConverterCommon, approved);
this.putMembershipNotificationToMetricConverter = new PutRoleMembershipDecisionNotificationToMetricConverter();
this.dbService = dbService;
}

@Override
public List<Notification> getNotifications() {
if (details == null) {
return new ArrayList<>();
}
// we need to send the notification to both the member whose pending membership was approved or rejected
// and also the member who requested the pending member
List<String> members = new ArrayList<>();
members.add(details.getOrDefault(NOTIFICATION_DETAILS_MEMBER, ""));
members.add(details.getOrDefault(NOTIFICATION_DETAILS_REQUESTER, ""));

Set<String> recipients = getRecipients(members);

return Collections.singletonList(notificationCommon.createNotification(
Notification.Type.ROLE_MEMBER_DECISION,
recipients,
details,
putMembershipNotificationToEmailConverter,
putMembershipNotificationToMetricConverter));
}

@Override
public String getDescription() {
return DESCRIPTION;
}

Set<String> getRecipients(List<String> members) {
Set<String> notifyMembers = new HashSet<>();
for (String memberName : members) {
if (StringUtils.isEmpty(memberName)) {
continue;
}
int idx = memberName.indexOf(AuthorityConsts.GROUP_SEP);
if (idx != -1) {
final String domainName = memberName.substring(0, idx);
final String groupName = memberName.substring(idx + AuthorityConsts.GROUP_SEP.length());
Group group = dbService.getGroup(domainName, groupName, Boolean.FALSE, Boolean.FALSE);
if (group == null) {
LOGGER.error("unable to retrieve group: {} in domain: {}", groupName, domainName);
continue;
}
if (!StringUtil.isEmpty(group.getNotifyRoles())) {
notifyMembers.addAll(NotificationUtils.extractNotifyRoleMembers(domainRoleMembersFetcher,
domainName, group.getNotifyRoles()));
} else {
notifyMembers.addAll(domainRoleMembersFetcher.getDomainRoleMembers(domainName, ADMIN_ROLE_NAME));
}
} else {
final String domainName = AthenzUtils.extractPrincipalDomainName(memberName);
if (userDomainPrefix.equals(domainName + ".")) {
notifyMembers.add(memberName);
} else {
// domain role fetcher only returns the human users
Set<String> domainAdminMembers = domainRoleMembersFetcher.getDomainRoleMembers(domainName, ADMIN_ROLE_NAME);
if (!ZMSUtils.isCollectionEmpty(domainAdminMembers)) {
for (String domainAdminMember : domainAdminMembers) {
notifyMembers.add(domainAdminMember);
}
}
}
}
}
return notifyMembers;
}

public static class PutRoleMembershipDecisionNotificationToEmailConverter implements NotificationToEmailConverter {
private static final String EMAIL_TEMPLATE_NOTIFICATION_APPROVAL = "messages/pending-role-membership-approve.html";
private static final String PENDING_MEMBERSHIP_APPROVAL_SUBJECT = "athenz.notification.email.pending_role_membership.decision.approval.subject";

private static final String EMAIL_TEMPLATE_NOTIFICATION_REJECT = "messages/pending-role-membership-reject.html";
private static final String PENDING_MEMBERSHIP_REJECT_SUBJECT = "athenz.notification.email.pending_role_membership.decision.reject.subject";

private final NotificationToEmailConverterCommon notificationToEmailConverterCommon;
private final String emailMembershipDecisionBody;
private final boolean pendingMemberApproved;

public PutRoleMembershipDecisionNotificationToEmailConverter(NotificationToEmailConverterCommon notificationToEmailConverterCommon, boolean approved) {
this.notificationToEmailConverterCommon = notificationToEmailConverterCommon;
pendingMemberApproved = approved;
emailMembershipDecisionBody = getEmailBody();
}

String getMembershipDecisionBody(Map<String, String> metaDetails) {
if (metaDetails == null) {
return null;
}
String athenzUIUrl = notificationToEmailConverterCommon.getAthenzUIUrl();
String body = MessageFormat.format(emailMembershipDecisionBody, metaDetails.get(NOTIFICATION_DETAILS_DOMAIN),
metaDetails.get(NOTIFICATION_DETAILS_ROLE), metaDetails.get(NOTIFICATION_DETAILS_MEMBER),
metaDetails.get(NOTIFICATION_DETAILS_REASON), metaDetails.get(NOTIFICATION_DETAILS_REQUESTER),
metaDetails.get(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_STATE),
metaDetails.get(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION_PRINCIPAL),
athenzUIUrl);
return notificationToEmailConverterCommon.addCssStyleToBody(body);
}

@Override
public NotificationEmail getNotificationAsEmail(Notification notification) {
String subject = notificationToEmailConverterCommon.getSubject(getNotificationSubjectProp());
String body = getMembershipDecisionBody(notification.getDetails());
Set<String> fullyQualifiedEmailAddresses = notificationToEmailConverterCommon.getFullyQualifiedEmailAddresses(notification.getRecipients());
return new NotificationEmail(subject, body, fullyQualifiedEmailAddresses);
}

String getEmailBody() {
if (pendingMemberApproved) {
return notificationToEmailConverterCommon.readContentFromFile(getClass().getClassLoader(), EMAIL_TEMPLATE_NOTIFICATION_APPROVAL);
} else {
return notificationToEmailConverterCommon.readContentFromFile(getClass().getClassLoader(), EMAIL_TEMPLATE_NOTIFICATION_REJECT);
}
}

String getNotificationSubjectProp() {
if (pendingMemberApproved) {
return PENDING_MEMBERSHIP_APPROVAL_SUBJECT;
} else {
return PENDING_MEMBERSHIP_REJECT_SUBJECT;
}
}
}

public static class PutRoleMembershipDecisionNotificationToMetricConverter implements NotificationToMetricConverter {
private final static String NOTIFICATION_TYPE = "pending_role_membership_decision";

@Override
public NotificationMetric getNotificationAsMetrics(Notification notification, Timestamp currentTime) {
String[] record = new String[] {
METRIC_NOTIFICATION_TYPE_KEY, NOTIFICATION_TYPE,
METRIC_NOTIFICATION_DOMAIN_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_DOMAIN),
METRIC_NOTIFICATION_ROLE_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_ROLE),
METRIC_NOTIFICATION_MEMBER_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_MEMBER),
METRIC_NOTIFICATION_REASON_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_REASON),
METRIC_NOTIFICATION_REQUESTER_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_REQUESTER),
METRIC_NOTIFICATION_MEMBERSHIP_DECISION, notification.getDetails().get(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION)
};

List<String[]> attributes = new ArrayList<>();
attributes.add(record);
return new NotificationMetric(attributes);
}
}
}
Loading

0 comments on commit 515c5fa

Please sign in to comment.