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