1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
93 protected static final long DEFAULT_URL_SIGNING_EXPIRE_DURATION = 2 * 60 * 60;
94
95
96 private static final Logger log = LoggerFactory.getLogger(SecurityEndpoint.class);
97
98 private long expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION;
99
100
101 private UrlSigningService urlSigningService;
102
103
104 @Reference
105 void setUrlSigningService(UrlSigningService urlSigningService) {
106 this.urlSigningService = urlSigningService;
107 }
108
109
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 }