001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.task;
018
019import static org.opends.messages.AdminToolMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Set;
028import java.util.TreeSet;
029
030import javax.naming.NamingException;
031import javax.naming.directory.Attribute;
032import javax.naming.directory.BasicAttribute;
033import javax.naming.directory.DirContext;
034import javax.naming.directory.ModificationItem;
035import javax.naming.ldap.InitialLdapContext;
036import javax.swing.SwingUtilities;
037import javax.swing.tree.TreePath;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.opendj.ldap.AVA;
041import org.forgerock.opendj.ldap.AttributeDescription;
042import org.forgerock.opendj.ldap.ByteString;
043import org.forgerock.opendj.ldap.DN;
044import org.forgerock.opendj.ldap.RDN;
045import org.forgerock.opendj.ldap.schema.AttributeType;
046import org.opends.guitools.controlpanel.browser.BrowserController;
047import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
048import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
049import org.opends.guitools.controlpanel.datamodel.CannotRenameException;
050import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
051import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
052import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
053import org.opends.guitools.controlpanel.ui.ProgressDialog;
054import org.opends.guitools.controlpanel.ui.StatusGenericPanel;
055import org.opends.guitools.controlpanel.ui.ViewEntryPanel;
056import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
057import org.opends.guitools.controlpanel.util.Utilities;
058import org.opends.messages.AdminToolMessages;
059import org.opends.server.types.Entry;
060import org.opends.server.types.Schema;
061
062/** The task that is called when we must modify an entry. */
063public class ModifyEntryTask extends Task
064{
065  private Set<String> backendSet;
066  private boolean mustRename;
067  private boolean hasModifications;
068  private CustomSearchResult oldEntry;
069  private DN oldDn;
070  private ArrayList<ModificationItem> modifications;
071  private ModificationItem passwordModification;
072  private Entry newEntry;
073  private BrowserController controller;
074  private TreePath treePath;
075  private boolean useAdminCtx;
076
077  /**
078   * Constructor of the task.
079   * @param info the control panel information.
080   * @param dlg the progress dialog where the task progress will be displayed.
081   * @param newEntry the entry containing the new values.
082   * @param oldEntry the old entry as we retrieved using JNDI.
083   * @param controller the BrowserController.
084   * @param path the TreePath corresponding to the node in the tree that we
085   * want to modify.
086   */
087  public ModifyEntryTask(ControlPanelInfo info, ProgressDialog dlg,
088      Entry newEntry, CustomSearchResult oldEntry,
089      BrowserController controller, TreePath path)
090  {
091    super(info, dlg);
092    backendSet = new HashSet<>();
093    this.oldEntry = oldEntry;
094    this.newEntry = newEntry;
095    this.controller = controller;
096    this.treePath = path;
097
098    DN newDn = newEntry.getName();
099    oldDn = DN.valueOf(oldEntry.getDN());
100    for (BackendDescriptor backend : info.getServerDescriptor().getBackends())
101    {
102      for (BaseDNDescriptor baseDN : backend.getBaseDns())
103      {
104        if (newDn.isSubordinateOrEqualTo(baseDN.getDn()) || oldDn.isSubordinateOrEqualTo(baseDN.getDn()))
105        {
106          backendSet.add(backend.getBackendID());
107        }
108      }
109    }
110    mustRename = !newDn.equals(oldDn);
111    modifications = getModifications(newEntry, oldEntry, getInfo());
112
113    // Find password modifications
114    for (ModificationItem mod : modifications)
115    {
116      if (mod.getAttribute().getID().equalsIgnoreCase("userPassword"))
117      {
118        passwordModification = mod;
119        break;
120      }
121    }
122    if (passwordModification != null)
123    {
124      modifications.remove(passwordModification);
125    }
126    hasModifications = !modifications.isEmpty()
127        || !oldDn.equals(newEntry.getName())
128        || passwordModification != null;
129  }
130
131  /**
132   * Tells whether there actually modifications on the entry.
133   * @return <CODE>true</CODE> if there are modifications and <CODE>false</CODE>
134   * otherwise.
135   */
136  public boolean hasModifications()
137  {
138    return hasModifications;
139  }
140
141  /** {@inheritDoc} */
142  public Type getType()
143  {
144    return Type.MODIFY_ENTRY;
145  }
146
147  /** {@inheritDoc} */
148  public Set<String> getBackends()
149  {
150    return backendSet;
151  }
152
153  /** {@inheritDoc} */
154  public LocalizableMessage getTaskDescription()
155  {
156    return INFO_CTRL_PANEL_MODIFY_ENTRY_TASK_DESCRIPTION.get(oldEntry.getDN());
157  }
158
159  /** {@inheritDoc} */
160  protected String getCommandLinePath()
161  {
162    return null;
163  }
164
165  /** {@inheritDoc} */
166  protected ArrayList<String> getCommandLineArguments()
167  {
168    return new ArrayList<>();
169  }
170
171  /** {@inheritDoc} */
172  public boolean canLaunch(Task taskToBeLaunched,
173      Collection<LocalizableMessage> incompatibilityReasons)
174  {
175    if (!isServerRunning()
176        && state == State.RUNNING
177        && runningOnSameServer(taskToBeLaunched))
178    {
179      // All the operations are incompatible if they apply to this
180      // backend for safety.  This is a short operation so the limitation
181      // has not a lot of impact.
182      Set<String> backends = new TreeSet<>(taskToBeLaunched.getBackends());
183      backends.retainAll(getBackends());
184      if (!backends.isEmpty())
185      {
186        incompatibilityReasons.add(getIncompatibilityMessage(this, taskToBeLaunched));
187        return false;
188      }
189    }
190    return true;
191  }
192
193  /** {@inheritDoc} */
194  public boolean regenerateDescriptor()
195  {
196    return false;
197  }
198
199  /** {@inheritDoc} */
200  public void runTask()
201  {
202    state = State.RUNNING;
203    lastException = null;
204
205    try
206    {
207      BasicNode node = (BasicNode)treePath.getLastPathComponent();
208      InitialLdapContext ctx = controller.findConnectionForDisplayedEntry(node);
209      useAdminCtx = controller.isConfigurationNode(node);
210      if (!mustRename)
211      {
212        if (!modifications.isEmpty()) {
213          ModificationItem[] mods =
214          new ModificationItem[modifications.size()];
215          modifications.toArray(mods);
216
217          SwingUtilities.invokeLater(new Runnable()
218          {
219            public void run()
220            {
221              printEquivalentCommandToModify(newEntry.getName(), modifications,
222                  useAdminCtx);
223              getProgressDialog().appendProgressHtml(
224                  Utilities.getProgressWithPoints(
225                      INFO_CTRL_PANEL_MODIFYING_ENTRY.get(oldEntry.getDN()),
226                      ColorAndFontConstants.progressFont));
227            }
228          });
229
230          ctx.modifyAttributes(Utilities.getJNDIName(oldEntry.getDN()), mods);
231
232          SwingUtilities.invokeLater(new Runnable()
233          {
234            public void run()
235            {
236              getProgressDialog().appendProgressHtml(
237                  Utilities.getProgressDone(
238                      ColorAndFontConstants.progressFont));
239              controller.notifyEntryChanged(
240                  controller.getNodeInfoFromPath(treePath));
241              controller.getTree().removeSelectionPath(treePath);
242              controller.getTree().setSelectionPath(treePath);
243            }
244          });
245        }
246      }
247      else
248      {
249        modifyAndRename(ctx, oldDn, oldEntry, newEntry, modifications);
250      }
251      state = State.FINISHED_SUCCESSFULLY;
252    }
253    catch (Throwable t)
254    {
255      lastException = t;
256      state = State.FINISHED_WITH_ERROR;
257    }
258  }
259
260  /** {@inheritDoc} */
261  public void postOperation()
262  {
263    if (lastException == null
264        && state == State.FINISHED_SUCCESSFULLY
265        && passwordModification != null)
266    {
267      try
268      {
269        Object o = passwordModification.getAttribute().get();
270        String sPwd;
271        if (o instanceof byte[])
272        {
273          try
274          {
275            sPwd = new String((byte[])o, "UTF-8");
276          }
277          catch (Throwable t)
278          {
279            throw new RuntimeException("Unexpected error: "+t, t);
280          }
281        }
282        else
283        {
284          sPwd = String.valueOf(o);
285        }
286        ResetUserPasswordTask newTask = new ResetUserPasswordTask(getInfo(),
287            getProgressDialog(), (BasicNode)treePath.getLastPathComponent(),
288            controller, sPwd.toCharArray());
289        if (!modifications.isEmpty() || mustRename)
290        {
291          getProgressDialog().appendProgressHtml("<br><br>");
292        }
293        StatusGenericPanel.launchOperation(newTask,
294            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUMMARY.get(),
295            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_SUMMARY.get(),
296            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_DETAILS.get(),
297            ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_SUMMARY.get(),
298            ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_DETAILS.get(),
299            null,
300            getProgressDialog(),
301            false,
302            getInfo());
303        getProgressDialog().setVisible(true);
304      }
305      catch (NamingException ne)
306      {
307        // This should not happen
308        throw new RuntimeException("Unexpected exception: "+ne, ne);
309      }
310    }
311  }
312
313  /**
314   * Modifies and renames the entry.
315   * @param ctx the connection to the server.
316   * @param oldDN the oldDN of the entry.
317   * @param originalEntry the original entry.
318   * @param newEntry the new entry.
319   * @param originalMods the original modifications (these are required since
320   * we might want to update them).
321   * @throws CannotRenameException if we cannot perform the modification.
322   * @throws NamingException if an error performing the modification occurs.
323   */
324  private void modifyAndRename(DirContext ctx, final DN oldDN,
325  CustomSearchResult originalEntry, final Entry newEntry,
326  final ArrayList<ModificationItem> originalMods)
327  throws CannotRenameException, NamingException
328  {
329    RDN oldRDN = oldDN.rdn();
330    RDN newRDN = newEntry.getName().rdn();
331
332    if (rdnTypeChanged(oldRDN, newRDN)
333        && userChangedObjectclass(originalMods)
334        /* See if the original entry contains the new naming attribute(s) if it does we will be able
335        to perform the renaming and then the modifications without problem */
336        && !entryContainsRdnTypes(originalEntry, newRDN))
337    {
338      throw new CannotRenameException(AdminToolMessages.ERR_CANNOT_MODIFY_OBJECTCLASS_AND_RENAME.get());
339    }
340
341    SwingUtilities.invokeLater(new Runnable()
342    {
343      public void run()
344      {
345        printEquivalentRenameCommand(oldDN, newEntry.getName(), useAdminCtx);
346        getProgressDialog().appendProgressHtml(
347            Utilities.getProgressWithPoints(
348                INFO_CTRL_PANEL_RENAMING_ENTRY.get(oldDN, newEntry.getName()),
349                ColorAndFontConstants.progressFont));
350      }
351    });
352
353    ctx.rename(Utilities.getJNDIName(oldDn.toString()),
354        Utilities.getJNDIName(newEntry.getName().toString()));
355
356    final TreePath[] newPath = {null};
357
358    SwingUtilities.invokeLater(new Runnable()
359    {
360      public void run()
361      {
362        getProgressDialog().appendProgressHtml(
363            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
364        getProgressDialog().appendProgressHtml("<br>");
365        TreePath parentPath = controller.notifyEntryDeleted(
366            controller.getNodeInfoFromPath(treePath));
367        newPath[0] = controller.notifyEntryAdded(
368            controller.getNodeInfoFromPath(parentPath),
369            newEntry.getName().toString());
370      }
371    });
372
373
374    ModificationItem[] mods = new ModificationItem[originalMods.size()];
375    originalMods.toArray(mods);
376    if (mods.length > 0)
377    {
378      SwingUtilities.invokeLater(new Runnable()
379      {
380        public void run()
381        {
382          DN dn = newEntry.getName();
383          printEquivalentCommandToModify(dn, originalMods, useAdminCtx);
384          getProgressDialog().appendProgressHtml(
385              Utilities.getProgressWithPoints(
386                  INFO_CTRL_PANEL_MODIFYING_ENTRY.get(dn),
387                  ColorAndFontConstants.progressFont));
388        }
389      });
390
391      ctx.modifyAttributes(Utilities.getJNDIName(newEntry.getName().toString()), mods);
392
393      SwingUtilities.invokeLater(new Runnable()
394      {
395        public void run()
396        {
397          getProgressDialog().appendProgressHtml(
398              Utilities.getProgressDone(ColorAndFontConstants.progressFont));
399          if (newPath[0] != null)
400          {
401            controller.getTree().setSelectionPath(newPath[0]);
402          }
403        }
404      });
405    }
406  }
407
408  private boolean rdnTypeChanged(RDN oldRDN, RDN newRDN)
409  {
410    if (newRDN.size() != oldRDN.size())
411    {
412      return true;
413    }
414
415    for (AVA ava : newRDN)
416    {
417      if (!find(oldRDN, ava.getAttributeType()))
418      {
419        return true;
420      }
421    }
422    return false;
423  }
424
425  private boolean find(RDN rdn, AttributeType attrType)
426  {
427    for (AVA ava : rdn)
428    {
429      if (attrType.equals(ava.getAttributeType()))
430      {
431        return true;
432      }
433    }
434    return false;
435  }
436
437  private boolean userChangedObjectclass(final ArrayList<ModificationItem> mods)
438  {
439    for (ModificationItem mod : mods)
440    {
441      if (ATTR_OBJECTCLASS.equalsIgnoreCase(mod.getAttribute().getID()))
442      {
443        return true;
444      }
445    }
446    return false;
447  }
448
449  private boolean entryContainsRdnTypes(CustomSearchResult entry, RDN rdn)
450  {
451    for (AVA ava : rdn)
452    {
453      List<Object> values = entry.getAttributeValues(ava.getAttributeName());
454      if (values.isEmpty())
455      {
456        return false;
457      }
458    }
459    return true;
460  }
461
462  /**
463   * Gets the modifications to apply between two entries.
464   * @param newEntry the new entry.
465   * @param oldEntry the old entry.
466   * @param info the ControlPanelInfo, used to retrieve the schema for instance.
467   * @return the modifications to apply between two entries.
468   */
469  public static ArrayList<ModificationItem> getModifications(Entry newEntry,
470      CustomSearchResult oldEntry, ControlPanelInfo info) {
471    ArrayList<ModificationItem> modifications = new ArrayList<>();
472    Schema schema = info.getServerDescriptor().getSchema();
473
474    List<org.opends.server.types.Attribute> newAttrs = newEntry.getAttributes();
475    newAttrs.add(newEntry.getObjectClassAttribute());
476    for (org.opends.server.types.Attribute attr : newAttrs)
477    {
478      AttributeDescription attrDesc = attr.getAttributeDescription();
479      String attrName = attrDesc.toString();
480      if (!ViewEntryPanel.isEditable(attrName, schema))
481      {
482        continue;
483      }
484      List<ByteString> newValues = new ArrayList<>();
485      Iterator<ByteString> it = attr.iterator();
486      while (it.hasNext())
487      {
488        newValues.add(it.next());
489      }
490      List<Object> oldValues = oldEntry.getAttributeValues(attrName);
491
492      ByteString rdnValue = null;
493      for (AVA ava : newEntry.getName().rdn())
494      {
495        if (ava.getAttributeType().equals(attrDesc.getAttributeType()))
496        {
497          rdnValue = ava.getAttributeValue();
498        }
499      }
500      boolean isAttributeInNewRdn = rdnValue != null;
501
502      /* Check the attributes of the old DN.  If we are renaming them they
503       * will be deleted.  Check that they are on the new entry but not in
504       * the new RDN. If it is the case we must add them after the renaming.
505       */
506      ByteString oldRdnValueToAdd = null;
507      /* Check the value in the RDN that will be deleted.  If the value was
508       * on the previous RDN but not in the new entry it will be deleted.  So
509       * we must avoid to include it as a delete modification in the
510       * modifications.
511       */
512      ByteString oldRdnValueDeleted = null;
513      RDN oldRDN = DN.valueOf(oldEntry.getDN()).rdn();
514      for (AVA ava : oldRDN)
515      {
516        if (ava.getAttributeType().equals(attrDesc.getAttributeType()))
517        {
518          ByteString value = ava.getAttributeValue();
519          if (attr.contains(value))
520          {
521            if (rdnValue == null || !rdnValue.equals(value))
522            {
523              oldRdnValueToAdd = value;
524            }
525          }
526          else
527          {
528            oldRdnValueDeleted = value;
529          }
530          break;
531        }
532      }
533      if (oldValues == null)
534      {
535        Set<ByteString> vs = new HashSet<>(newValues);
536        if (rdnValue != null)
537        {
538          vs.remove(rdnValue);
539        }
540        if (!vs.isEmpty())
541        {
542          modifications.add(new ModificationItem(
543              DirContext.ADD_ATTRIBUTE,
544              createAttribute(attrName, newValues)));
545        }
546      } else {
547        List<ByteString> toDelete = getValuesToDelete(oldValues, newValues);
548        if (oldRdnValueDeleted != null)
549        {
550          toDelete.remove(oldRdnValueDeleted);
551        }
552        List<ByteString> toAdd = getValuesToAdd(oldValues, newValues);
553        if (oldRdnValueToAdd != null)
554        {
555          toAdd.add(oldRdnValueToAdd);
556        }
557        if (toDelete.size() + toAdd.size() >= newValues.size() &&
558            !isAttributeInNewRdn)
559        {
560          modifications.add(new ModificationItem(
561              DirContext.REPLACE_ATTRIBUTE,
562              createAttribute(attrName, newValues)));
563        }
564        else
565        {
566          if (!toDelete.isEmpty())
567          {
568            modifications.add(new ModificationItem(
569                DirContext.REMOVE_ATTRIBUTE,
570                createAttribute(attrName, toDelete)));
571          }
572          if (!toAdd.isEmpty())
573          {
574            List<ByteString> vs = new ArrayList<>(toAdd);
575            if (rdnValue != null)
576            {
577              vs.remove(rdnValue);
578            }
579            if (!vs.isEmpty())
580            {
581              modifications.add(new ModificationItem(
582                  DirContext.ADD_ATTRIBUTE,
583                  createAttribute(attrName, vs)));
584            }
585          }
586        }
587      }
588    }
589
590    /* Check if there are attributes to delete */
591    for (String attrName : oldEntry.getAttributeNames())
592    {
593      if (!ViewEntryPanel.isEditable(attrName, schema))
594      {
595        continue;
596      }
597      List<Object> oldValues = oldEntry.getAttributeValues(attrName);
598      String attrNoOptions =
599        Utilities.getAttributeNameWithoutOptions(attrName).toLowerCase();
600
601      List<org.opends.server.types.Attribute> attrs = newEntry.getAttribute(attrNoOptions);
602      if (!find(attrs, attrName) && !oldValues.isEmpty())
603      {
604        modifications.add(new ModificationItem(
605            DirContext.REMOVE_ATTRIBUTE,
606            new BasicAttribute(attrName)));
607      }
608    }
609    return modifications;
610  }
611
612  private static boolean find(List<org.opends.server.types.Attribute> attrs, String attrName)
613  {
614    for (org.opends.server.types.Attribute attr : attrs)
615    {
616      if (attr.getNameWithOptions().equalsIgnoreCase(attrName))
617      {
618        return true;
619      }
620    }
621    return false;
622  }
623
624  /**
625   * Creates a JNDI attribute using an attribute name and a set of values.
626   * @param attrName the attribute name.
627   * @param values the values.
628   * @return a JNDI attribute using an attribute name and a set of values.
629   */
630  private static Attribute createAttribute(String attrName, List<ByteString> values) {
631    Attribute attribute = new BasicAttribute(attrName);
632    for (ByteString value : values)
633    {
634      attribute.add(value.toByteArray());
635    }
636    return attribute;
637  }
638
639  /**
640   * Creates a ByteString for an attribute and a value (the one we got using JNDI).
641   * @param value the value found using JNDI.
642   * @return a ByteString object.
643   */
644  private static ByteString createAttributeValue(Object value)
645  {
646    if (value instanceof String)
647    {
648      return ByteString.valueOfUtf8((String) value);
649    }
650    else if (value instanceof byte[])
651    {
652      return ByteString.wrap((byte[]) value);
653    }
654    return ByteString.valueOfUtf8(String.valueOf(value));
655  }
656
657  /**
658   * Returns the set of ByteString that must be deleted.
659   * @param oldValues the old values of the entry.
660   * @param newValues the new values of the entry.
661   * @return the set of ByteString that must be deleted.
662   */
663  private static List<ByteString> getValuesToDelete(List<Object> oldValues,
664      List<ByteString> newValues)
665  {
666    List<ByteString> valuesToDelete = new ArrayList<>();
667    for (Object o : oldValues)
668    {
669      ByteString oldValue = createAttributeValue(o);
670      if (!newValues.contains(oldValue))
671      {
672        valuesToDelete.add(oldValue);
673      }
674    }
675    return valuesToDelete;
676  }
677
678  /**
679   * Returns the set of ByteString that must be added.
680   * @param oldValues the old values of the entry.
681   * @param newValues the new values of the entry.
682   * @return the set of ByteString that must be added.
683   */
684  private static List<ByteString> getValuesToAdd(List<Object> oldValues,
685    List<ByteString> newValues)
686  {
687    List<ByteString> valuesToAdd = new ArrayList<>();
688    for (ByteString newValue : newValues)
689    {
690      if (!contains(oldValues, newValue))
691      {
692        valuesToAdd.add(newValue);
693      }
694    }
695    return valuesToAdd;
696  }
697
698  private static boolean contains(List<Object> oldValues, ByteString newValue)
699  {
700    for (Object o : oldValues)
701    {
702      if (createAttributeValue(o).equals(newValue))
703      {
704        return true;
705      }
706    }
707    return false;
708  }
709}