001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2010 Sun Microsystems, Inc.
025 *      Portions copyright 2012-2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.ldap.controls;
028
029import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
030import static com.forgerock.opendj.ldap.CoreMessages.*;
031
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.LinkedList;
037import java.util.List;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizedIllegalArgumentException;
041import org.forgerock.opendj.io.ASN1;
042import org.forgerock.opendj.io.ASN1Reader;
043import org.forgerock.opendj.io.ASN1Writer;
044import org.forgerock.opendj.ldap.ByteString;
045import org.forgerock.opendj.ldap.ByteStringBuilder;
046import org.forgerock.opendj.ldap.DN;
047import org.forgerock.opendj.ldap.DecodeException;
048import org.forgerock.opendj.ldap.DecodeOptions;
049import org.forgerock.opendj.ldap.schema.AttributeType;
050import org.forgerock.opendj.ldap.schema.Schema;
051import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
052
053import org.forgerock.util.Reject;
054
055/**
056 * A partial implementation of the get effective rights request control as
057 * defined in draft-ietf-ldapext-acl-model. The main differences are:
058 * <ul>
059 * <li>The response control is not supported. Instead the OpenDJ implementation
060 * creates attributes containing effective rights information with the entry
061 * being returned.
062 * <li>The attribute type names are dynamically created.
063 * <li>The set of attributes for which effective rights information is to be
064 * requested can be included in the control.
065 * </ul>
066 * The get effective rights request control value has the following BER
067 * encoding:
068 *
069 * <pre>
070 *  GetRightsControl ::= SEQUENCE {
071 *    authzId    authzId  -- Only the "dn:DN" form is supported.
072 *    attributes  SEQUENCE OF AttributeType
073 *  }
074 * </pre>
075 *
076 * You can use the control to retrieve effective rights during a search:
077 *
078 * <pre>
079 * String authDN = ...;
080 *
081 * SearchRequest request =
082 *         Requests.newSearchRequest(
083 *                     "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
084 *                     "(uid=bjensen)", "cn", "aclRights", "aclRightsInfo")
085 *                     .addControl(GetEffectiveRightsRequestControl.newControl(
086 *                             true, authDN, "cn"));
087 *
088 * ConnectionEntryReader reader = connection.search(request);
089 * while (reader.hasNext()) {
090 *      if (!reader.isReference()) {
091 *          SearchResultEntry entry = reader.readEntry();
092 *          // Interpret aclRights and aclRightsInfo
093 *      }
094 * }
095 * </pre>
096 *
097 * The entries returned by the search hold the {@code aclRights} and
098 * {@code aclRightsInfo} attributes with the effective rights information. You
099 * must parse the attribute options and values to interpret the information.
100 *
101 * @see <a
102 *      href="http://tools.ietf.org/html/draft-ietf-ldapext-acl-model">draft-ietf-ldapext-acl-model
103 *      - Access Control Model for LDAPv3 </a>
104 **/
105public final class GetEffectiveRightsRequestControl implements Control {
106    /**
107     * The OID for the get effective rights request control.
108     */
109    public static final String OID = "1.3.6.1.4.1.42.2.27.9.5.2";
110
111    /**
112     * A decoder which can be used for decoding the get effective rights request
113     * control.
114     */
115    public static final ControlDecoder<GetEffectiveRightsRequestControl> DECODER =
116            new ControlDecoder<GetEffectiveRightsRequestControl>() {
117
118                public GetEffectiveRightsRequestControl decodeControl(final Control control,
119                        final DecodeOptions options) throws DecodeException {
120                    Reject.ifNull(control);
121
122                    if (control instanceof GetEffectiveRightsRequestControl) {
123                        return (GetEffectiveRightsRequestControl) control;
124                    }
125
126                    if (!control.getOID().equals(OID)) {
127                        final LocalizableMessage message =
128                                ERR_GETEFFECTIVERIGHTS_CONTROL_BAD_OID.get(control.getOID(), OID);
129                        throw DecodeException.error(message);
130                    }
131
132                    DN authorizationDN = null;
133                    List<AttributeType> attributes = Collections.emptyList();
134
135                    if (control.hasValue()) {
136                        final ASN1Reader reader = ASN1.getReader(control.getValue());
137                        try {
138                            reader.readStartSequence();
139                            final String authzIDString = reader.readOctetStringAsString();
140                            final String lowerAuthzIDString = authzIDString.toLowerCase();
141                            Schema schema;
142
143                            // Make sure authzId starts with "dn:" and is a
144                            // valid DN.
145                            if (lowerAuthzIDString.startsWith("dn:")) {
146                                final String authorizationDNString = authzIDString.substring(3);
147                                schema =
148                                        options.getSchemaResolver().resolveSchema(
149                                                authorizationDNString);
150                                try {
151                                    authorizationDN = DN.valueOf(authorizationDNString, schema);
152                                } catch (final LocalizedIllegalArgumentException e) {
153                                    final LocalizableMessage message =
154                                            ERR_GETEFFECTIVERIGHTS_INVALID_AUTHZIDDN
155                                                    .get(getExceptionMessage(e));
156                                    throw DecodeException.error(message, e);
157                                }
158                            } else {
159                                final LocalizableMessage message =
160                                        INFO_GETEFFECTIVERIGHTS_INVALID_AUTHZID
161                                                .get(lowerAuthzIDString);
162                                throw DecodeException.error(message);
163                            }
164
165                            // There is an sequence containing an attribute list, try to decode it.
166                            if (reader.hasNextElement()) {
167                                attributes = new LinkedList<>();
168                                reader.readStartSequence();
169                                while (reader.hasNextElement()) {
170                                    // Decode as an attribute type.
171                                    final String attributeName = reader.readOctetStringAsString();
172                                    AttributeType attributeType;
173                                    try {
174                                        // FIXME: we're using the schema
175                                        // associated with the authzid
176                                        // which is not really correct. We
177                                        // should really use the schema
178                                        // associated with the entry.
179                                        attributeType = schema.getAttributeType(attributeName);
180                                    } catch (final UnknownSchemaElementException e) {
181                                        final LocalizableMessage message =
182                                                ERR_GETEFFECTIVERIGHTS_UNKNOWN_ATTRIBUTE
183                                                        .get(attributeName);
184                                        throw DecodeException.error(message, e);
185                                    }
186                                    attributes.add(attributeType);
187                                }
188                                reader.readEndSequence();
189                                attributes = Collections.unmodifiableList(attributes);
190                            }
191                            reader.readEndSequence();
192                        } catch (final IOException e) {
193                            final LocalizableMessage message =
194                                    INFO_GETEFFECTIVERIGHTS_DECODE_ERROR.get(e.getMessage());
195                            throw DecodeException.error(message);
196                        }
197                    }
198
199                    return new GetEffectiveRightsRequestControl(control.isCritical(),
200                            authorizationDN, attributes);
201
202                }
203
204                public String getOID() {
205                    return OID;
206                }
207            };
208
209    /**
210     * Creates a new get effective rights request control with the provided
211     * criticality, optional authorization name and attribute list.
212     *
213     * @param isCritical
214     *            {@code true} if it is unacceptable to perform the operation
215     *            without applying the semantics of this control, or
216     *            {@code false} if it can be ignored.
217     * @param authorizationName
218     *            The distinguished name of the user for which effective rights
219     *            are to be returned, or {@code null} if the client's
220     *            authentication ID is to be used.
221     * @param attributes
222     *            The list of attributes for which effective rights are to be
223     *            returned, which may be empty indicating that no attribute
224     *            rights are to be returned.
225     * @return The new control.
226     * @throws NullPointerException
227     *             If {@code attributes} was {@code null}.
228     */
229    public static GetEffectiveRightsRequestControl newControl(final boolean isCritical,
230            final DN authorizationName, final Collection<AttributeType> attributes) {
231        Reject.ifNull(attributes);
232
233        final Collection<AttributeType> copyOfAttributes =
234                Collections.unmodifiableList(new ArrayList<AttributeType>(attributes));
235        return new GetEffectiveRightsRequestControl(isCritical, authorizationName, copyOfAttributes);
236    }
237
238    /**
239     * Creates a new get effective rights request control with the provided
240     * criticality, optional authorization name and attribute list. The
241     * authorization name and attributes, if provided, will be decoded using the
242     * default schema.
243     *
244     * @param isCritical
245     *            {@code true} if it is unacceptable to perform the operation
246     *            without applying the semantics of this control, or
247     *            {@code false} if it can be ignored.
248     * @param authorizationName
249     *            The distinguished name of the user for which effective rights
250     *            are to be returned, or {@code null} if the client's
251     *            authentication ID is to be used.
252     * @param attributes
253     *            The list of attributes for which effective rights are to be
254     *            returned, which may be empty indicating that no attribute
255     *            rights are to be returned.
256     * @return The new control.
257     * @throws UnknownSchemaElementException
258     *             If the default schema is a strict schema and one or more of
259     *             the requested attribute types were not recognized.
260     * @throws LocalizedIllegalArgumentException
261     *             If {@code authorizationName} is not a valid LDAP string
262     *             representation of a DN.
263     * @throws NullPointerException
264     *             If {@code attributes} was {@code null}.
265     */
266    public static GetEffectiveRightsRequestControl newControl(final boolean isCritical,
267            final String authorizationName, final String... attributes) {
268        Reject.ifNull((Object) attributes);
269
270        final DN dn = authorizationName == null ? null : DN.valueOf(authorizationName);
271
272        List<AttributeType> copyOfAttributes;
273        if (attributes != null && attributes.length > 0) {
274            copyOfAttributes = new ArrayList<>(attributes.length);
275            for (final String attribute : attributes) {
276                copyOfAttributes.add(Schema.getDefaultSchema().getAttributeType(attribute));
277            }
278            copyOfAttributes = Collections.unmodifiableList(copyOfAttributes);
279        } else {
280            copyOfAttributes = Collections.emptyList();
281        }
282
283        return new GetEffectiveRightsRequestControl(isCritical, dn, copyOfAttributes);
284    }
285
286    /** The DN representing the authzId (may be null meaning use the client's DN). */
287    private final DN authorizationName;
288
289    /** The unmodifiable list of attributes to be queried (may be empty). */
290    private final Collection<AttributeType> attributes;
291
292    private final boolean isCritical;
293
294    private GetEffectiveRightsRequestControl(final boolean isCritical, final DN authorizationName,
295            final Collection<AttributeType> attributes) {
296        this.isCritical = isCritical;
297        this.authorizationName = authorizationName;
298        this.attributes = attributes;
299    }
300
301    /**
302     * Returns an unmodifiable list of attributes for which effective rights are
303     * to be returned, which may be empty indicating that no attribute rights
304     * are to be returned.
305     *
306     * @return The unmodifiable list of attributes for which effective rights
307     *         are to be returned.
308     */
309    public Collection<AttributeType> getAttributes() {
310        return attributes;
311    }
312
313    /**
314     * Returns the distinguished name of the user for which effective rights are
315     * to be returned, or {@code null} if the client's authentication ID is to
316     * be used.
317     *
318     * @return The distinguished name of the user for which effective rights are
319     *         to be returned.
320     */
321    public DN getAuthorizationName() {
322        return authorizationName;
323    }
324
325    /** {@inheritDoc} */
326    public String getOID() {
327        return OID;
328    }
329
330    /** {@inheritDoc} */
331    public ByteString getValue() {
332        final ByteStringBuilder buffer = new ByteStringBuilder();
333        final ASN1Writer writer = ASN1.getWriter(buffer);
334        try {
335            writer.writeStartSequence();
336            if (authorizationName != null) {
337                writer.writeOctetString("dn:" + authorizationName);
338            }
339
340            if (!attributes.isEmpty()) {
341                writer.writeStartSequence();
342                for (final AttributeType attribute : attributes) {
343                    writer.writeOctetString(attribute.getNameOrOID());
344                }
345                writer.writeEndSequence();
346            }
347            writer.writeEndSequence();
348            return buffer.toByteString();
349        } catch (final IOException ioe) {
350            // This should never happen unless there is a bug somewhere.
351            throw new RuntimeException(ioe);
352        }
353    }
354
355    /** {@inheritDoc} */
356    public boolean hasValue() {
357        return authorizationName != null || !attributes.isEmpty();
358    }
359
360    /** {@inheritDoc} */
361    public boolean isCritical() {
362        return isCritical;
363    }
364
365    /** {@inheritDoc} */
366    @Override
367    public String toString() {
368        final StringBuilder builder = new StringBuilder();
369        builder.append("GetEffectiveRightsRequestControl(oid=");
370        builder.append(getOID());
371        builder.append(", criticality=");
372        builder.append(isCritical());
373        builder.append(", authorizationDN=\"");
374        builder.append(authorizationName);
375        builder.append("\"");
376        builder.append(", attributes=(");
377        builder.append(attributes);
378        builder.append("))");
379        return builder.toString();
380    }
381}