blob: cc386a5d393008e974a623cda63a1bef8140a0c7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.syncope.core.provisioning.java.job.report;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.report.ReconciliationReportletConf;
import org.apache.syncope.common.lib.report.ReconciliationReportletConf.Feature;
import org.apache.syncope.common.lib.report.ReportletConf;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.core.persistence.api.dao.AnyDAO;
import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.ReportletConfClass;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyType;
import org.apache.syncope.core.persistence.api.entity.AnyUtils;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.group.Group;
import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
import org.apache.syncope.core.persistence.api.entity.resource.Provision;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
import org.apache.syncope.core.provisioning.api.Connector;
import org.apache.syncope.core.provisioning.api.ConnectorManager;
import org.apache.syncope.core.provisioning.api.MappingManager;
import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.Uid;
import org.springframework.beans.factory.annotation.Autowired;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* Reportlet for extracting information for the current reconciliation status, e.g. the coherence between Syncope
* information and mapped entities on external resources.
*/
@ReportletConfClass(ReconciliationReportletConf.class)
public class ReconciliationReportlet extends AbstractReportlet {
private static final int PAGE_SIZE = 10;
@Autowired
private UserDAO userDAO;
@Autowired
private GroupDAO groupDAO;
@Autowired
private AnyTypeDAO anyTypeDAO;
@Autowired
private AnySearchDAO searchDAO;
@Autowired
private MappingManager mappingManager;
@Autowired
private ConnectorManager connectorManager;
@Autowired
private AnyUtilsFactory anyUtilsFactory;
@Autowired
private SearchCondVisitor searchCondVisitor;
private ReconciliationReportletConf conf;
private static String getAnyElementName(final AnyTypeKind anyTypeKind) {
String elementName;
switch (anyTypeKind) {
case USER:
elementName = "user";
break;
case GROUP:
elementName = "group";
break;
case ANY_OBJECT:
default:
elementName = "anyObject";
}
return elementName;
}
private void doExtract(
final ContentHandler handler,
final Any<?> any,
final Set<Missing> missing,
final Set<Misaligned> misaligned)
throws SAXException {
AttributesImpl atts = new AttributesImpl();
for (Feature feature : conf.getFeatures()) {
String type = null;
String value = null;
switch (feature) {
case key:
type = ReportXMLConst.XSD_STRING;
value = any.getKey();
break;
case username:
if (any instanceof User) {
type = ReportXMLConst.XSD_STRING;
value = User.class.cast(any).getUsername();
}
break;
case groupName:
if (any instanceof Group) {
type = ReportXMLConst.XSD_STRING;
value = Group.class.cast(any).getName();
}
break;
case status:
type = ReportXMLConst.XSD_STRING;
value = any.getStatus();
break;
case creationDate:
type = ReportXMLConst.XSD_DATETIME;
value = any.getCreationDate() == null
? StringUtils.EMPTY
: FormatUtils.format(any.getCreationDate());
break;
case lastLoginDate:
if (any instanceof User) {
type = ReportXMLConst.XSD_DATETIME;
value = User.class.cast(any).getLastLoginDate() == null
? StringUtils.EMPTY
: FormatUtils.format(User.class.cast(any).getLastLoginDate());
}
break;
case changePwdDate:
if (any instanceof User) {
type = ReportXMLConst.XSD_DATETIME;
value = User.class.cast(any).getChangePwdDate() == null
? StringUtils.EMPTY
: FormatUtils.format(User.class.cast(any).getChangePwdDate());
}
break;
case passwordHistorySize:
if (any instanceof User) {
type = ReportXMLConst.XSD_INT;
value = String.valueOf(User.class.cast(any).getPasswordHistory().size());
}
break;
case failedLoginCount:
if (any instanceof User) {
type = ReportXMLConst.XSD_INT;
value = String.valueOf(User.class.cast(any).getFailedLogins());
}
break;
default:
}
if (type != null && value != null) {
atts.addAttribute("", "", feature.name(), type, value);
}
}
handler.startElement("", "", getAnyElementName(any.getType().getKind()), atts);
for (Missing item : missing) {
atts.clear();
atts.addAttribute("", "", "resource", ReportXMLConst.XSD_STRING, item.getResource());
atts.addAttribute("", "", "connObjectKeyValue", ReportXMLConst.XSD_STRING, item.getConnObjectKeyValue());
handler.startElement("", "", "missing", atts);
handler.endElement("", "", "missing");
}
for (Misaligned item : misaligned) {
atts.clear();
atts.addAttribute("", "", "resource", ReportXMLConst.XSD_STRING, item.getResource());
atts.addAttribute("", "", "connObjectKeyValue", ReportXMLConst.XSD_STRING, item.getConnObjectKeyValue());
atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, item.getName());
handler.startElement("", "", "misaligned", atts);
handler.startElement("", "", "onSyncope", null);
if (item.getOnSyncope() != null) {
for (Object value : item.getOnSyncope()) {
char[] asChars = value.toString().toCharArray();
handler.startElement("", "", "value", null);
handler.characters(asChars, 0, asChars.length);
handler.endElement("", "", "value");
}
}
handler.endElement("", "", "onSyncope");
handler.startElement("", "", "onResource", null);
if (item.getOnResource() != null) {
for (Object value : item.getOnResource()) {
char[] asChars = value.toString().toCharArray();
handler.startElement("", "", "value", null);
handler.characters(asChars, 0, asChars.length);
handler.endElement("", "", "value");
}
}
handler.endElement("", "", "onResource");
handler.endElement("", "", "misaligned");
}
handler.endElement("", "", getAnyElementName(any.getType().getKind()));
}
private static Set<Object> getValues(final Attribute attr) {
Set<Object> values;
if (attr.getValue() == null || attr.getValue().isEmpty()) {
values = Set.of();
} else if (attr.getValue().get(0) instanceof byte[]) {
values = new HashSet<>(attr.getValue().size());
attr.getValue().forEach(single -> values.add(Base64.getEncoder().encode((byte[]) single)));
} else {
values = new HashSet<>(attr.getValue());
}
return values;
}
private void doExtract(final ContentHandler handler, final List<? extends Any<?>> anys)
throws SAXException, ReportException {
final Set<Missing> missing = new HashSet<>();
final Set<Misaligned> misaligned = new HashSet<>();
for (Any<?> any : anys) {
missing.clear();
misaligned.clear();
AnyUtils anyUtils = anyUtilsFactory.getInstance(any);
anyUtils.getAllResources(any).forEach(resource -> {
Provision provision = resource.getProvision(any.getType()).orElse(null);
Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
final String connObjectKeyValue = connObjectKeyItem.isPresent()
? mappingManager.getConnObjectKeyValue(any, provision).get()
: StringUtils.EMPTY;
if (provision != null && connObjectKeyItem.isPresent() && StringUtils.isNotBlank(connObjectKeyValue)) {
// 1. read from the underlying connector
Connector connector = connectorManager.getConnector(resource);
ConnectorObject connectorObject = connector.getObject(
provision.getObjectClass(),
AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue),
provision.isIgnoreCaseMatch(),
MappingUtils.buildOperationOptions(provision.getMapping().getItems().stream()));
if (connectorObject == null) {
// 2. not found on resource?
LOG.error("Object {} with class {} not found on resource {}",
connObjectKeyValue, provision.getObjectClass(), resource);
missing.add(new Missing(resource.getKey(), connObjectKeyValue));
} else {
// 3. found but misaligned?
Pair<String, Set<Attribute>> preparedAttrs =
mappingManager.prepareAttrsFromAny(any, null, false, null, provision);
preparedAttrs.getRight().add(AttributeBuilder.build(
Uid.NAME, preparedAttrs.getLeft()));
preparedAttrs.getRight().add(AttributeBuilder.build(
connObjectKeyItem.get().getExtAttrName(), preparedAttrs.getLeft()));
final Map<String, Set<Object>> syncopeAttrs = new HashMap<>();
preparedAttrs.getRight().forEach(attr -> syncopeAttrs.put(attr.getName(), getValues(attr)));
final Map<String, Set<Object>> resourceAttrs = new HashMap<>();
connectorObject.getAttributes().stream().
filter(attr -> (!OperationalAttributes.PASSWORD_NAME.equals(attr.getName())
&& !OperationalAttributes.ENABLE_NAME.equals(attr.getName()))).
forEachOrdered(attr -> resourceAttrs.put(attr.getName(), getValues(attr)));
syncopeAttrs.keySet().stream().
filter(syncopeAttr -> !resourceAttrs.containsKey(syncopeAttr)).
forEach(name -> misaligned.add(new Misaligned(
resource.getKey(),
connObjectKeyValue,
name,
syncopeAttrs.get(name),
Set.of())));
resourceAttrs.forEach((key, values) -> {
if (syncopeAttrs.containsKey(key)) {
if (!Objects.equals(syncopeAttrs.get(key), values)) {
misaligned.add(new Misaligned(
resource.getKey(),
connObjectKeyValue,
key,
syncopeAttrs.get(key),
values));
}
} else {
misaligned.add(new Misaligned(
resource.getKey(),
connObjectKeyValue,
key,
Set.of(),
values));
}
});
}
}
});
if (!missing.isEmpty() || !misaligned.isEmpty()) {
doExtract(handler, any, missing, misaligned);
}
}
}
@Override
protected void doExtract(
final ReportletConf conf,
final ContentHandler handler,
final AtomicReference<String> status)
throws SAXException {
if (conf instanceof ReconciliationReportletConf) {
this.conf = ReconciliationReportletConf.class.cast(conf);
} else {
throw new ReportException(new IllegalArgumentException("Invalid configuration provided"));
}
AttributesImpl atts = new AttributesImpl();
if (StringUtils.isBlank(this.conf.getUserMatchingCond())) {
int total = userDAO.count();
int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
status.set("Processing " + total + " users in " + pages + " pages");
atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(total));
handler.startElement("", "", getAnyElementName(AnyTypeKind.USER) + 's', atts);
for (int page = 1; page <= pages; page++) {
status.set("Processing " + total + " users: page " + page + " of " + pages);
doExtract(handler, userDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE));
}
} else {
SearchCond cond = SearchCondConverter.convert(searchCondVisitor, this.conf.getUserMatchingCond());
int total = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.USER);
int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
status.set("Processing " + total + " users in " + pages + " pages");
atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(total));
handler.startElement("", "", getAnyElementName(AnyTypeKind.USER) + 's', atts);
for (int page = 1; page <= pages; page++) {
status.set("Processing " + total + " users: page " + page + " of " + pages);
doExtract(handler, searchDAO.search(
SyncopeConstants.FULL_ADMIN_REALMS,
cond,
page,
PAGE_SIZE,
List.of(),
AnyTypeKind.USER));
}
}
handler.endElement("", "", getAnyElementName(AnyTypeKind.USER) + 's');
atts.clear();
if (StringUtils.isBlank(this.conf.getGroupMatchingCond())) {
int total = groupDAO.count();
int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
status.set("Processing " + total + " groups in " + pages + " pages");
atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(total));
handler.startElement("", "", getAnyElementName(AnyTypeKind.GROUP) + 's', atts);
for (int page = 1; page <= pages; page++) {
status.set("Processing " + total + " groups: page " + page + " of " + pages);
doExtract(handler, groupDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE));
}
} else {
SearchCond cond = SearchCondConverter.convert(searchCondVisitor, this.conf.getUserMatchingCond());
int total = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.GROUP);
int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
status.set("Processing " + total + " groups in " + pages + " pages");
atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(total));
handler.startElement("", "", getAnyElementName(AnyTypeKind.GROUP) + 's', atts);
for (int page = 1; page <= pages; page++) {
status.set("Processing " + total + " groups: page " + page + " of " + pages);
doExtract(handler, searchDAO.search(
SyncopeConstants.FULL_ADMIN_REALMS,
cond,
page,
PAGE_SIZE,
List.of(),
AnyTypeKind.GROUP));
}
}
handler.endElement("", "", getAnyElementName(AnyTypeKind.GROUP) + 's');
for (AnyType anyType : anyTypeDAO.findAll()) {
if (!anyType.equals(anyTypeDAO.findUser()) && !anyType.equals(anyTypeDAO.findGroup())) {
AnyTypeCond anyTypeCond = new AnyTypeCond();
anyTypeCond.setAnyTypeKey(anyType.getKey());
SearchCond cond = StringUtils.isBlank(this.conf.getAnyObjectMatchingCond())
? SearchCond.getLeaf(anyTypeCond)
: SearchCond.getAnd(
SearchCond.getLeaf(anyTypeCond),
SearchCondConverter.convert(searchCondVisitor, this.conf.getAnyObjectMatchingCond()));
int total = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.ANY_OBJECT);
int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
status.set("Processing " + total + " any objects " + anyType.getKey() + " in " + pages + " pages");
atts.clear();
atts.addAttribute("", "", "type", ReportXMLConst.XSD_STRING, anyType.getKey());
atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(total));
handler.startElement("", "", getAnyElementName(AnyTypeKind.ANY_OBJECT) + 's', atts);
for (int page = 1; page <= pages; page++) {
status.set("Processing " + total + " any objects " + anyType.getKey()
+ ": page " + page + " of " + pages);
doExtract(handler, searchDAO.search(
SyncopeConstants.FULL_ADMIN_REALMS,
cond,
page,
PAGE_SIZE,
List.of(),
AnyTypeKind.ANY_OBJECT));
}
handler.endElement("", "", getAnyElementName(AnyTypeKind.ANY_OBJECT) + 's');
}
}
}
private static class Missing {
private final String resource;
private final String connObjectKeyValue;
Missing(final String resource, final String connObjectKeyValue) {
this.resource = resource;
this.connObjectKeyValue = connObjectKeyValue;
}
public String getResource() {
return resource;
}
public String getConnObjectKeyValue() {
return connObjectKeyValue;
}
}
private static class Misaligned extends Missing {
private final String name;
private final Set<Object> onSyncope;
private final Set<Object> onResource;
Misaligned(
final String resource,
final String connObjectKeyValue,
final String name,
final Set<Object> onSyncope,
final Set<Object> onResource) {
super(resource, connObjectKeyValue);
this.name = name;
this.onSyncope = onSyncope;
this.onResource = onResource;
}
public String getName() {
return name;
}
public Set<Object> getOnSyncope() {
return onSyncope;
}
public Set<Object> getOnResource() {
return onResource;
}
}
}