Skip to content

Commit

Permalink
send notification for put group membership decision (#2742)
Browse files Browse the repository at this point in the history
Signed-off-by: craman <[email protected]>
Co-authored-by: craman <[email protected]>
  • Loading branch information
chandrasekhar1996 and craman authored Sep 28, 2024
1 parent 901537a commit caba2bf
Show file tree
Hide file tree
Showing 17 changed files with 2,236 additions and 831 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ athenz.notification.email.group_member.expiry.subject=Athenz Group Member Expira
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

athenz.notification.email.pending_group_membership.decision.reject.subject=Athenz Pending Group Member Rejected

athenz.notification.email.pending_group_membership.decision.approval.subject=Athenz Pending Group Member Approved
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
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

athenz.notification.email.pending_group_membership.decision.reject.subject=Athenz Pending Group Member Rejected
athenz.notification.email.pending_group_membership.decision.approval.subject=Athenz Pending Group 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 @@ -9743,6 +9743,21 @@ RoleMember getPendingRoleMember(String domainName, String roleName, String membe
}
}

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

class UserAuthorityFilterEnforcer implements Runnable {

public UserAuthorityFilterEnforcer() {
Expand Down
38 changes: 34 additions & 4 deletions servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@
import com.yahoo.athenz.common.server.util.config.providers.ConfigProviderFile;
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.notification.*;
import com.yahoo.athenz.zms.provider.DomainDependencyProviderResponse;
import com.yahoo.athenz.zms.provider.ServiceProviderClient;
import com.yahoo.athenz.zms.provider.ServiceProviderManager;
Expand Down Expand Up @@ -5062,6 +5059,33 @@ void sendRoleMembershipDecisionNotification(final String domain, final String ro
notificationManager.sendNotifications(notifications);
}

void sendGroupMembershipDecisionNotification(final String domain, final String groupName,
final GroupMember groupMember, 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_GROUP, groupName);
details.put(NOTIFICATION_DETAILS_MEMBER, groupMember.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 = groupMember.getApproved() ? ZMSConsts.PENDING_REQUEST_APPROVE : ZMSConsts.PENDING_REQUEST_REJECT;
details.put(NOTIFICATION_DETAILS_PENDING_MEMBERSHIP_DECISION, membershipDecision);

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

List<Notification> notifications = new PutGroupMembershipDecisionNotificationTask(details,
groupMember.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 @@ -11567,7 +11591,13 @@ public void putGroupMembershipDecision(ResourceContext ctx, String domainName, S
userAuthorityFilterSet, principalDomainFilter, caller);
}

//get the pending group member details to send notification
GroupMember pendingMember = dbService.getPendingGroupMember(domainName, groupName, memberName);

dbService.executePutGroupMembershipDecision(ctx, domainName, group, groupMember, auditRef);

sendGroupMembershipDecisionNotification(domainName, groupName,
groupMember, auditRef, principal.getFullName(), pendingMember.getPendingState(), pendingMember.getRequestPrincipal());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.DomainRoleMembersFetcher;
import com.yahoo.athenz.zms.DBService;
import com.yahoo.athenz.zms.Group;
import com.yahoo.athenz.zms.utils.ZMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.yahoo.athenz.common.ServerCommonConsts.ADMIN_ROLE_NAME;

public class MembershipDecisionNotificationCommon {
private static final Logger LOGGER = LoggerFactory.getLogger(MembershipDecisionNotificationCommon.class);
private final DBService dbService;
private final DomainRoleMembersFetcher domainRoleMembersFetcher;
private final String userDomainPrefix;

MembershipDecisionNotificationCommon(DBService dbService, DomainRoleMembersFetcher domainRoleMembersFetcher, String userDomainPrefix) {
this.dbService = dbService;
this.domainRoleMembersFetcher = domainRoleMembersFetcher;
this.userDomainPrefix = userDomainPrefix;
}

public 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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.common.server.notification.*;
import com.yahoo.athenz.zms.DBService;
import com.yahoo.rdl.Timestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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 PutGroupMembershipDecisionNotificationTask 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 Group Membership Decision Notification";
private final PutGroupMembershipDecisionNotificationToEmailConverter putMembershipNotificationToEmailConverter;
private final PutGroupMembershipDecisionNotificationToMetricConverter putMembershipNotificationToMetricConverter;
private final DBService dbService;
private final DomainRoleMembersFetcher domainRoleMembersFetcher;
private final String userDomainPrefix;

public PutGroupMembershipDecisionNotificationTask(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 PutGroupMembershipDecisionNotificationToEmailConverter(notificationToEmailConverterCommon, approved);
this.putMembershipNotificationToMetricConverter = new PutGroupMembershipDecisionNotificationToMetricConverter();
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, ""));

MembershipDecisionNotificationCommon membershipDecisionNotificationCommon = new MembershipDecisionNotificationCommon(dbService, domainRoleMembersFetcher, userDomainPrefix);
Set<String> recipients = membershipDecisionNotificationCommon.getRecipients(members);

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

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

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

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

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

public PutGroupMembershipDecisionNotificationToEmailConverter(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_GROUP), 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 PutGroupMembershipDecisionNotificationToMetricConverter implements NotificationToMetricConverter {
private final static String NOTIFICATION_TYPE = "pending_group_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_GROUP_KEY, notification.getDetails().get(NOTIFICATION_DETAILS_GROUP),
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 caba2bf

Please sign in to comment.