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 2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap.authz;
017
018import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.asConditionalFilter;
019import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter;
020import static org.forgerock.util.promise.Promises.newResultPromise;
021
022import java.net.URI;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.Set;
026
027import org.forgerock.openig.oauth2.AccessTokenInfo;
028import org.forgerock.openig.oauth2.AccessTokenResolver;
029import org.forgerock.openig.oauth2.OAuth2Context;
030import org.forgerock.openig.oauth2.ResourceAccess;
031import org.forgerock.openig.oauth2.ResourceServerFilter;
032import org.forgerock.http.Filter;
033import org.forgerock.http.Handler;
034import org.forgerock.http.filter.Filters;
035import org.forgerock.http.protocol.Headers;
036import org.forgerock.http.protocol.Request;
037import org.forgerock.http.protocol.Response;
038import org.forgerock.http.protocol.ResponseException;
039import org.forgerock.http.protocol.Status;
040import org.forgerock.opendj.ldap.Connection;
041import org.forgerock.opendj.ldap.ConnectionFactory;
042import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
043import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
044import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.Condition;
045import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
046import org.forgerock.services.context.Context;
047import org.forgerock.services.context.SecurityContext;
048import org.forgerock.util.Function;
049import org.forgerock.util.Pair;
050import org.forgerock.util.Reject;
051import org.forgerock.util.promise.NeverThrowsException;
052import org.forgerock.util.promise.Promise;
053import org.forgerock.util.time.TimeService;
054
055/** Factory methods to create {@link Filter} performing authentication and authorizations. */
056public final class Authorization {
057
058    private static final String OAUTH2_AUTHORIZATION_HEADER = "Authorization";
059
060    /**
061     * Creates a new {@link Filter} in charge of injecting an {@link AuthenticatedConnectionContext}. This
062     * {@link Filter} tries each of the provided filters until one can apply. If no filter can be applied, the last
063     * filter in the list will be applied allowing it to formulate a valid, implementation specific, error response.
064     *
065     * @param filters
066     *            {@link Iterable} of authorization {@link ConditionalFilters} to try. If empty, the returned filter
067     *            will always respond with 403 Forbidden.
068     * @return A new authorization {@link Filter}
069     */
070    public static Filter newAuthorizationFilter(Iterable<? extends ConditionalFilter> filters) {
071        return new AuthorizationFilter(filters);
072    }
073
074    /**
075     * Creates a new {@link ConditionalFilter} performing authentication. If authentication succeed, it injects a
076     * {@link SecurityContext} with the authenticationId provided by the user. Otherwise, returns a HTTP 401 -
077     * Unauthorized response. The condition of this {@link ConditionalFilter} will return true if the supplied requests
078     * contains credentials information, false otherwise.
079     *
080     * @param authenticationStrategy
081     *            {@link AuthenticationStrategy} to validate the user's provided credentials.
082     * @param credentialsExtractor
083     *            Function to extract the credentials from the received request.
084     * @throws NullPointerException
085     *             if a parameter is null.
086     * @return a new {@link ConditionalFilter}
087     */
088    public static ConditionalFilter newConditionalHttpBasicAuthenticationFilter(
089            final AuthenticationStrategy authenticationStrategy,
090            final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor) {
091        return newConditionalFilter(
092                new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor),
093                new Condition() {
094                    @Override
095                    public boolean canApplyFilter(Context context, Request request) {
096                        return credentialsExtractor.apply(request.getHeaders()) != null;
097                    }
098                });
099    }
100
101    /**
102     * Creates a {@link ConditionalFilter} injecting an {@link AuthenticatedConnectionContext} with a connection issued
103     * from the given connectionFactory. The condition is always true.
104     *
105     * @param connectionFactory
106     *            The factory used to get the {@link Connection} to inject.
107     * @return A new {@link ConditionalFilter}.
108     * @throws NullPointerException
109     *             if connectionFactory is null
110     */
111    public static ConditionalFilter newConditionalDirectConnectionFilter(ConnectionFactory connectionFactory) {
112        return asConditionalFilter(new DirectConnectionFilter(connectionFactory));
113    }
114
115    /**
116     * Creates a filter injecting an {@link AuthenticatedConnectionContext} given the information provided in the
117     * {@link SecurityContext}. The connection contained in the created {@link AuthenticatedConnectionContext} will add
118     * a {@link ProxiedAuthV2RequestControl} to each LDAP requests.
119     *
120     * @param connectionFactory
121     *            The connection factory used to create the connection which will be injected in the
122     *            {@link AuthenticatedConnectionContext}
123     * @return A new filter.
124     * @throws NullPointerException
125     *             if connectionFactory is null
126     */
127    public static Filter newProxyAuthorizationFilter(ConnectionFactory connectionFactory) {
128        return new ProxiedAuthV2Filter(connectionFactory);
129    }
130
131    /**
132     * Creates a new {@link AccessTokenResolver} as defined in the RFC-7662.
133     * <p>
134     * @see <a href="https://tools.ietf.org/html/rfc7662">RFC-7662</a>
135     *
136     * @param httpClient
137     *          Http client handler used to perform the request
138     * @param introspectionEndPointURL
139     *          Introspect endpoint URL to use to resolve the access token.
140     * @param clientAppId
141     *          Client application id to use in HTTP Basic authentication header.
142     * @param clientAppSecret
143     *          Client application secret to use in HTTP Basic authentication header.
144     * @return A new {@link AccessTokenResolver} instance.
145     */
146    public static AccessTokenResolver newRfc7662AccessTokenResolver(final Handler httpClient,
147                                                                    final URI introspectionEndPointURL,
148                                                                    final String clientAppId,
149                                                                    final String clientAppSecret) {
150        return new Rfc7662AccessTokenResolver(httpClient, introspectionEndPointURL, clientAppId, clientAppSecret);
151    }
152
153    /**
154     * Creates a new CTS access token resolver.
155     *
156     * @param connectionFactory
157     *          The {@link ConnectionFactory} to use to perform search against the CTS.
158     * @param ctsBaseDNTemplate
159     *          The base DN template to use to resolve the access token DN.
160     * @return A new CTS access token resolver.
161     */
162    public static AccessTokenResolver newCtsAccessTokenResolver(final ConnectionFactory connectionFactory,
163                                                                final String ctsBaseDNTemplate) {
164        return new CtsAccessTokenResolver(connectionFactory, ctsBaseDNTemplate);
165    }
166
167    /**
168     * Creates a new file access token resolver which should only be used for test purpose.
169     *
170     * @param tokenFolder
171     *          The folder where the access token to resolve must be stored.
172     * @return A new file access token resolver which should only be used for test purpose.
173     */
174    public static AccessTokenResolver newFileAccessTokenResolver(final String tokenFolder) {
175        return new FileAccessTokenResolver(tokenFolder);
176    }
177
178    /**
179     * Creates a new OAuth2 authorization filter configured with provided parameters.
180     *
181     * @param realm
182     *          The realm to displays in error responses.
183     * @param scopes
184     *          Scopes that an access token must have to be access a resource.
185     * @param resolver
186     *          The {@link AccessTokenResolver} to use to resolve an access token.
187     * @param authzIdTemplate
188     *          Authorization ID template.
189     * @return A new OAuth2 authorization filter configured with provided parameters.
190     */
191    public static Filter newOAuth2ResourceServerFilter(final String realm,
192                                                                  final Set<String> scopes,
193                                                                  final AccessTokenResolver resolver,
194                                                                  final String authzIdTemplate) {
195        return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate);
196    }
197
198    /**
199     * Creates a new optional OAuth2 authorization filter configured with provided parameters.
200     * <p>
201     * This filter will be used only if an OAuth2 Authorization header is present in the incoming request.
202     *
203     * @param realm
204     *          The realm to displays in error responses.
205     * @param scopes
206     *          Scopes that an access token must have to be access a resource.
207     * @param resolver
208     *          The {@link AccessTokenResolver} to use to resolve an access token.
209     * @param authzIdTemplate
210     *          Authorization ID template.
211     * @return A new OAuth2 authorization filter configured with provided parameters.
212     */
213    public static ConditionalFilter newConditionalOAuth2ResourceServerFilter(final String realm,
214                                                                             final Set<String> scopes,
215                                                                             final AccessTokenResolver resolver,
216                                                                             final String authzIdTemplate) {
217        return new ConditionalFilter() {
218            @Override
219            public Filter getFilter() {
220                return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate);
221            }
222
223            @Override
224            public Condition getCondition() {
225                return new Condition() {
226                    @Override
227                    public boolean canApplyFilter(final Context context, final Request request) {
228                        return request.getHeaders().containsKey(OAUTH2_AUTHORIZATION_HEADER);
229                    }
230                };
231            }
232        };
233    }
234
235    private static Filter createResourceServerFilter(final String realm,
236                                                     final Set<String> scopes,
237                                                     final AccessTokenResolver resolver,
238                                                     final String authzIdTemplate) {
239        Reject.ifTrue(realm == null || realm.isEmpty(), "realm must not be empty");
240        Reject.ifNull(resolver, "Access token resolver must not be null");
241        Reject.ifTrue(scopes == null || scopes.isEmpty(), "scopes set can not be empty");
242        Reject.ifTrue(authzIdTemplate == null || authzIdTemplate.isEmpty(), "Authz id template must not be empty");
243
244        final ResourceAccess scopesProvider = new ResourceAccess() {
245            @Override
246            public Set<String> getRequiredScopes(final Context context, final Request request)
247                    throws ResponseException {
248                return scopes;
249            }
250        };
251
252        return Filters.chainOf(new ResourceServerFilter(resolver, TimeService.SYSTEM, scopesProvider, realm),
253                               createSecurityContextInjectionFilter(authzIdTemplate));
254    }
255
256    private static Filter createSecurityContextInjectionFilter(final String authzIdTemplate) {
257        final AuthzIdTemplate template = new AuthzIdTemplate(authzIdTemplate);
258
259        return new Filter() {
260            @Override
261            public Promise<Response, NeverThrowsException> filter(final Context context,
262                                                                  final Request request,
263                                                                  final Handler next) {
264                final AccessTokenInfo token = context.asContext(OAuth2Context.class).getAccessToken();
265                final Map<String, Object> authz = new HashMap<>(1);
266                try {
267                    authz.put(template.getSecurityContextID(), template.formatAsAuthzId(token.asJsonValue()));
268                } catch (final IllegalArgumentException e) {
269                    return newResultPromise(new Response().setStatus(Status.INTERNAL_SERVER_ERROR)
270                                                          .setCause(e));
271                }
272                final Context securityContext = new SecurityContext(context, token.getToken(), authz);
273                return next.handle(securityContext, request);
274            }
275        };
276    }
277
278    private Authorization() {
279        // Prevent instantiation.
280    }
281}