1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.security.urlsigning.provider.impl;
22
23 import org.opencastproject.security.api.Organization;
24 import org.opencastproject.security.api.SecurityService;
25 import org.opencastproject.security.urlsigning.exception.UrlSigningException;
26 import org.opencastproject.security.urlsigning.provider.UrlSigningProvider;
27 import org.opencastproject.urlsigning.common.Policy;
28 import org.opencastproject.urlsigning.common.ResourceStrategy;
29 import org.opencastproject.urlsigning.utils.ResourceRequestUtil;
30
31 import org.apache.commons.lang3.StringUtils;
32 import org.apache.commons.lang3.exception.ExceptionUtils;
33 import org.apache.http.NameValuePair;
34 import org.apache.http.client.utils.URLEncodedUtils;
35 import org.osgi.service.cm.ConfigurationException;
36 import org.osgi.service.cm.ManagedService;
37 import org.slf4j.Logger;
38
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.nio.charset.StandardCharsets;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.Dictionary;
46 import java.util.Enumeration;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.Set;
52 import java.util.TreeMap;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55
56
57 public abstract class AbstractUrlSigningProvider implements UrlSigningProvider, ManagedService {
58
59 public static final String KEY_PROPERTY_PREFIX = "key";
60
61
62 public static final String SECRET = "secret";
63
64
65 public static final String URL = "url";
66
67
68 public static final String ORGANIZATION = "organization";
69
70
71 public static final String ANY_ORGANIZATION = "*";
72
73
74 public static final String EXCLUSION_PROPERTY_KEY = "exclude.url.pattern";
75
76
77 protected SecurityService securityService;
78
79
80
81
82 public abstract ResourceStrategy getResourceStrategy();
83
84
85
86
87 public abstract Logger getLogger();
88
89
90
91
92 protected static class Key {
93 private String id = null;
94 private String secret = null;
95 private String organizationId = ANY_ORGANIZATION;
96
97 Key(String id) {
98 this.id = id;
99 }
100
101 public String getSecret() {
102 return secret;
103 }
104
105 boolean supports(String organizationId) {
106 return this.organizationId.equals(ANY_ORGANIZATION) || this.organizationId.equals(organizationId);
107 }
108 }
109
110
111 protected TreeMap<String, Key> urls = new TreeMap<>();
112
113
114 private Pattern exclusionPattern;
115
116
117
118
119
120 public void setSecurityService(SecurityService securityService) {
121 this.securityService = securityService;
122 }
123
124
125
126
127 public Set<String> getUris() {
128 return Collections.unmodifiableSet(urls.keySet());
129 }
130
131
132
133
134
135
136
137
138
139
140 protected Key getKey(String baseUrl) {
141
142
143
144 Map.Entry<String, Key> candidate = urls.floorEntry(baseUrl);
145 if (candidate != null && baseUrl.startsWith(candidate.getKey())) {
146 Key key = candidate.getValue();
147
148
149
150 Organization organization = securityService.getOrganization();
151 if (organization != null && key.supports(organization.getId())) {
152 return key;
153 }
154 }
155 return null;
156 }
157
158 @Override
159 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
160 getLogger().info("Updating {}", toString());
161 if (properties == null) {
162 getLogger().warn("{} is unconfigured", toString());
163 return;
164 }
165
166
167 TreeMap<String, Key> urls = new TreeMap<>();
168 Pattern exclusionPattern = null;
169
170
171 Map<String, Key> keys = new HashMap<>();
172
173 Enumeration<String> propertyKeys = properties.keys();
174 while (propertyKeys.hasMoreElements()) {
175 String propertyKey = propertyKeys.nextElement();
176
177 if (propertyKey.startsWith(KEY_PROPERTY_PREFIX + ".")) {
178
179
180 String[] parts = Arrays.stream(propertyKey.split("\\.")).map(String::trim).toArray(String[]::new);
181 if ((parts.length != 3) && !(parts.length == 4 && URL.equals(parts[2]))) {
182 throw new ConfigurationException(propertyKey, "Wrong property key format");
183 }
184
185 String propertyValue = StringUtils.trimToNull(Objects.toString(properties.get(propertyKey), null));
186 if (propertyValue == null) {
187 throw new ConfigurationException(propertyKey, "Can't be null or empty");
188 }
189
190 String id = parts[1];
191 Key currentKey = keys.computeIfAbsent(id, __ -> new Key(id));
192
193 String attribute = parts[2];
194 switch (attribute) {
195 case ORGANIZATION:
196 currentKey.organizationId = propertyValue;
197 break;
198 case URL:
199 if (urls.keySet().stream().anyMatch(v -> propertyValue.startsWith(v) || (v.startsWith(propertyValue)))) {
200 throw new ConfigurationException(propertyKey,
201 "There is already a key configuration for a URL with the prefix " + propertyValue);
202 }
203
204 urls.put(propertyValue, currentKey);
205 break;
206 case SECRET:
207 currentKey.secret = propertyValue;
208 break;
209 default:
210 throw new ConfigurationException(propertyKey, "Unknown attribute " + attribute + " for key " + id);
211 }
212 } else if (EXCLUSION_PROPERTY_KEY.equals(propertyKey)) {
213 String propertyValue = Objects.toString(properties.get(propertyKey), "");
214 if (!StringUtils.isEmpty(propertyValue)) {
215 exclusionPattern = Pattern.compile(propertyValue);
216 }
217 getLogger().debug("Exclusion pattern: {}", propertyValue);
218 }
219 }
220
221
222 for (Key key : keys.values()) {
223 if (key.secret == null) {
224 throw new ConfigurationException(key.id, "No secret set");
225 }
226 }
227
228
229 if (urls.size() == 0) {
230 getLogger().info("{} configured to not sign any urls.", toString());
231 } else {
232 getLogger().info("{} configured to sign urls.", toString());
233 }
234
235 this.urls = urls;
236 this.exclusionPattern = exclusionPattern;
237 }
238
239
240
241
242 private boolean isExcluded(String url) {
243 boolean isExcluded = false;
244 Pattern exclusionPattern = this.exclusionPattern;
245 if (exclusionPattern != null) {
246 Matcher matcher = exclusionPattern.matcher(url);
247 isExcluded = matcher.matches();
248 }
249 return isExcluded;
250 }
251
252
253
254
255 private boolean isValid(String url) {
256 try {
257 new URI(url);
258 return true;
259 } catch (URISyntaxException e) {
260 getLogger().debug("Unable to support url {} because", url, e);
261 return false;
262 }
263 }
264
265
266
267
268 @Override
269 public boolean accepts(String baseUrl) {
270 return isValid(baseUrl) && !isExcluded(baseUrl) && getKey(baseUrl) != null;
271 }
272
273
274
275
276 @Override
277 public String sign(Policy policy) throws UrlSigningException {
278 String url = policy.getBaseUrl();
279 Key key = getKey(url);
280 if (isExcluded(url) || key == null) {
281 throw UrlSigningException.urlNotSupported();
282 }
283
284 policy.setResourceStrategy(getResourceStrategy());
285
286 try {
287 URI uri = new URI(url);
288 List<NameValuePair> queryStringParameters = new ArrayList<>();
289 if (uri.getQuery() != null) {
290 queryStringParameters = URLEncodedUtils.parse(uri.getQuery(), StandardCharsets.UTF_8);
291 }
292 queryStringParameters.addAll(URLEncodedUtils.parse(
293 ResourceRequestUtil.policyToResourceRequestQueryString(policy, key.id, key.secret),
294 StandardCharsets.UTF_8));
295 return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(),
296 URLEncodedUtils.format(queryStringParameters, StandardCharsets.UTF_8), null).toString();
297 } catch (Exception e) {
298 getLogger().error("Unable to create signed URL because {}", ExceptionUtils.getStackTrace(e));
299 throw new UrlSigningException(e);
300 }
301 }
302 }