View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  package org.opencastproject.external.endpoint;
22  
23  import static org.apache.commons.lang3.StringUtils.isBlank;
24  import static org.apache.commons.lang3.StringUtils.isNotBlank;
25  import static org.opencastproject.util.DateTimeSupport.fromUTC;
26  import static org.opencastproject.util.DateTimeSupport.toUTC;
27  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
28  
29  import org.opencastproject.external.common.ApiMediaType;
30  import org.opencastproject.external.common.ApiResponseBuilder;
31  import org.opencastproject.security.urlsigning.exception.UrlSigningException;
32  import org.opencastproject.security.urlsigning.service.UrlSigningService;
33  import org.opencastproject.util.DateTimeSupport;
34  import org.opencastproject.util.OsgiUtil;
35  import org.opencastproject.util.RestUtil.R;
36  import org.opencastproject.util.doc.rest.RestParameter;
37  import org.opencastproject.util.doc.rest.RestQuery;
38  import org.opencastproject.util.doc.rest.RestResponse;
39  import org.opencastproject.util.doc.rest.RestService;
40  
41  import com.google.gson.JsonObject;
42  
43  import org.joda.time.DateTime;
44  import org.joda.time.DateTimeConstants;
45  import org.osgi.service.cm.ConfigurationException;
46  import org.osgi.service.cm.ManagedService;
47  import org.osgi.service.component.annotations.Activate;
48  import org.osgi.service.component.annotations.Component;
49  import org.osgi.service.component.annotations.Reference;
50  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  import java.text.ParseException;
55  import java.util.Date;
56  import java.util.Dictionary;
57  import java.util.Optional;
58  
59  import javax.servlet.http.HttpServletResponse;
60  import javax.ws.rs.FormParam;
61  import javax.ws.rs.HeaderParam;
62  import javax.ws.rs.POST;
63  import javax.ws.rs.Path;
64  import javax.ws.rs.Produces;
65  import javax.ws.rs.core.Response;
66  
67  @Path("/api/security")
68  @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
69              ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
70              ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
71              ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
72  @RestService(
73      name = "externalapisecurity",
74      title = "External API Security Service",
75      notes = {},
76      abstractText = "Provides security operations related to the external API"
77  )
78  @Component(
79      immediate = true,
80      service = { SecurityEndpoint.class,ManagedService.class },
81      property = {
82          "service.description=External API - Security Endpoint",
83          "opencast.service.type=org.opencastproject.external.security",
84          "opencast.service.path=/api/security"
85      }
86  )
87  @JaxrsResource
88  public class SecurityEndpoint implements ManagedService {
89  
90    protected static final String URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY = "url.signing.expires.seconds";
91  
92    /** The default time before a piece of signed content expires. 2 Hours. */
93    protected static final long DEFAULT_URL_SIGNING_EXPIRE_DURATION = 2 * 60 * 60;
94  
95    /** The logging facility */
96    private static final Logger log = LoggerFactory.getLogger(SecurityEndpoint.class);
97  
98    private long expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION;
99  
100   /* OSGi service references */
101   private UrlSigningService urlSigningService;
102 
103   /** OSGi DI */
104   @Reference
105   void setUrlSigningService(UrlSigningService urlSigningService) {
106     this.urlSigningService = urlSigningService;
107   }
108 
109   /** OSGi activation method */
110   @Activate
111   void activate() {
112     log.info("Activating External API - Security Endpoint");
113   }
114 
115   @Override
116   public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
117     if (properties == null) {
118       log.info("No configuration available, using defaults");
119       return;
120     }
121 
122     Optional<Long> expiration = OsgiUtil.getOptCfg(properties, URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY)
123             .map(Long::parseLong);
124     if (expiration.isPresent()) {
125       expireSeconds = expiration.get();
126       log.info("The property {} has been configured to expire signed URLs in {}.",
127               URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY, DateTimeSupport.humanReadableTime(expireSeconds));
128     } else {
129       expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION;
130       log.info("The property {} has not been configured, so the default is being used to expire signed URLs in {}.",
131               URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY, DateTimeSupport.humanReadableTime(expireSeconds));
132     }
133   }
134 
135   @POST
136   @Path("sign")
137   @RestQuery(
138       name = "signurl",
139       description = "Returns a signed URL that can be played back for the indicated period of time, while access is "
140           + "optionally restricted to the specified IP address.",
141       returnDescription = "",
142       restParameters = {
143           @RestParameter(name = "url", isRequired = true, description = "The linke to encode.", type = STRING),
144           @RestParameter(name = "valid-until", description = "Until when is the signed url valid", isRequired = false,
145               type = STRING),
146           @RestParameter(name = "valid-source", description = "The IP address from which the url can be accessed",
147               isRequired = false, type = STRING)
148       },
149       responses = {
150           @RestResponse(description = "The signed URL is returned.", responseCode = HttpServletResponse.SC_OK),
151           @RestResponse(description = "The caller is not authorized to have the link signed.",
152               responseCode = HttpServletResponse.SC_UNAUTHORIZED)
153       })
154   public Response signUrl(@HeaderParam("Accept") String acceptHeader, @FormParam("url") String url,
155           @FormParam("valid-until") String validUntilUtc, @FormParam("valid-source") String validSource) {
156     if (isBlank(url)) {
157       return R.badRequest("Query parameter 'url' is mandatory");
158     }
159 
160     final DateTime validUntil;
161     if (isNotBlank(validUntilUtc)) {
162       try {
163         validUntil = new DateTime(fromUTC(validUntilUtc));
164       } catch (IllegalStateException | ParseException e) {
165         return R.badRequest("Query parameter 'valid-until' is not a valid ISO-8601 date string");
166       }
167     } else {
168       validUntil = new DateTime(new Date().getTime() + expireSeconds * DateTimeConstants.MILLIS_PER_SECOND);
169     }
170 
171     if (urlSigningService.accepts(url)) {
172       String signedUrl = "";
173       try {
174         signedUrl = urlSigningService.sign(url, validUntil, null, validSource);
175       } catch (UrlSigningException e) {
176         log.warn("Error while trying to sign url '{}':", url, e);
177         JsonObject errorJson = new JsonObject();
178         errorJson.addProperty("error", "Error while signing url");
179         return ApiResponseBuilder.Json.ok(acceptHeader, errorJson);
180       }
181 
182       JsonObject successJson = new JsonObject();
183       successJson.addProperty("url", signedUrl);
184       successJson.addProperty("valid-until", toUTC(validUntil.getMillis()));
185       return ApiResponseBuilder.Json.ok(acceptHeader, successJson);
186     } else {
187       JsonObject errorJson = new JsonObject();
188       errorJson.addProperty("error", "Given URL cannot be signed");
189       return ApiResponseBuilder.Json.ok(acceptHeader, errorJson);
190     }
191   }
192 }