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  
22  package org.opencastproject.transcription.microsoft.azure;
23  
24  import org.apache.commons.codec.binary.Base64;
25  import org.apache.commons.codec.digest.HmacAlgorithms;
26  import org.apache.commons.codec.digest.HmacUtils;
27  import org.apache.commons.lang3.StringUtils;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  import java.net.URLEncoder;
32  import java.nio.charset.StandardCharsets;
33  import java.nio.file.Paths;
34  import java.text.SimpleDateFormat;
35  import java.time.Instant;
36  import java.time.temporal.ChronoUnit;
37  import java.util.ArrayList;
38  import java.util.Date;
39  import java.util.List;
40  import java.util.TimeZone;
41  
42  import javax.crypto.Mac;
43  
44  public class MicrosoftAzureAuthorization {
45  
46    private static final Logger logger = LoggerFactory.getLogger(MicrosoftAzureStorageClient.class);
47    public static final String AZURE_STORAGE_VERSION = "2022-11-02";
48    public static final String AZURE_BLOB_STORE_URL_SUFFIX = "blob.core.windows.net";
49  
50    private final String azureStorageAccountName;
51    private final String azureAccountAccessKey;
52  
53    public MicrosoftAzureAuthorization(String azureStorageAccountName, String azureAccountAccessKey)
54            throws MicrosoftAzureStorageClientException {
55      this.azureStorageAccountName = StringUtils.trimToEmpty(azureStorageAccountName);
56      this.azureAccountAccessKey = StringUtils.trimToEmpty(azureAccountAccessKey);
57      if (StringUtils.isEmpty(azureStorageAccountName)) {
58        throw new MicrosoftAzureStorageClientException("Azure storage account name not set.");
59      }
60      if (StringUtils.isEmpty(azureAccountAccessKey)) {
61        throw new MicrosoftAzureStorageClientException("Azure storage account key not set.");
62      }
63    }
64  
65    public String getAzureStorageAccountName() {
66      return azureStorageAccountName;
67    }
68  
69    String generateAccountSASToken(String signedPermissions, String signedResourceType,
70        Date signedStart, Date signedExpiry, String signedIP, String signedEncryptionScope) {
71      // documentation: https://learn.microsoft.com/en-us/rest/api/storageservices/create-account-sas
72      SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
73      df.setTimeZone(TimeZone.getTimeZone("UTC"));
74      List<String> queryArgs = new ArrayList<>();
75      StringBuilder stringBuilder = new StringBuilder();
76      /*
77      StringToSign = accountname + "\n" +
78          signedpermissions + "\n" +
79          signedservice + "\n" +
80          signedresourcetype + "\n" +
81          signedstart + "\n" +
82          signedexpiry + "\n" +
83          signedIP + "\n" +
84          signedProtocol + "\n" +
85          signedversion + "\n" +
86          signedEncryptionScope + "\n"
87      */
88      stringBuilder.append(azureStorageAccountName + "\n");
89      stringBuilder.append(StringUtils.trimToEmpty(signedPermissions) + "\n");
90      queryArgs.add("sp=" + StringUtils.trimToEmpty(signedPermissions));
91      stringBuilder.append("b" + "\n");
92      queryArgs.add("ss=b");
93      stringBuilder.append(StringUtils.trimToEmpty(signedResourceType) + "\n");
94      queryArgs.add("srt=" + StringUtils.trimToEmpty(signedResourceType));
95      Date startDate = signedStart;
96      if (startDate == null) {
97        startDate = Date.from(Instant.now().minus(15, ChronoUnit.MINUTES));
98      }
99      stringBuilder.append(df.format(startDate) + "\n");
100     queryArgs.add("st=" + df.format(startDate));
101     Date endDate = signedExpiry;
102     if (endDate == null) {
103       endDate = Date.from(Instant.now().plus(1, ChronoUnit.DAYS));
104     }
105     stringBuilder.append(df.format(endDate) + "\n");
106     queryArgs.add("se=" + df.format(endDate));
107     stringBuilder.append(StringUtils.trimToEmpty(signedIP) + "\n");
108     if (StringUtils.isNotBlank(signedIP)) {
109       queryArgs.add("sip=" + StringUtils.trimToEmpty(signedIP));
110     }
111     stringBuilder.append("https" + "\n");
112     queryArgs.add("spr=https");
113     stringBuilder.append(AZURE_STORAGE_VERSION + "\n");
114     queryArgs.add("sv=" + AZURE_STORAGE_VERSION);
115     stringBuilder.append(StringUtils.trimToEmpty(signedEncryptionScope) + "\n");
116     if (StringUtils.isNotBlank(signedEncryptionScope)) {
117       queryArgs.add("ses=" + StringUtils.trimToEmpty(signedEncryptionScope));
118     }
119     String stringToSign = stringBuilder.toString();
120 
121     Mac initializedMac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256,
122         Base64.decodeBase64(azureAccountAccessKey));
123     byte[] signedString = initializedMac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
124     String signature = Base64.encodeBase64String(signedString);
125     queryArgs.add("sig=" + URLEncoder.encode(signature, StandardCharsets.UTF_8));
126     return StringUtils.joinWith("&", queryArgs.toArray());
127   }
128 
129   String generateServiceSasToken(String signedPermissions, Date signedStart, Date signedExpiry, String resource,
130       String signedResource) {
131     return  generateServiceSasToken(signedPermissions, signedStart, signedExpiry, resource, null, null, null,
132         signedResource, null, null, null, null, null, null, null, null);
133   }
134 
135   String generateServiceSasToken(String signedPermissions, Date signedStart, Date signedExpiry, String resource,
136       String signedIdentifier, String signedIP, String signedVersion, String signedResource,
137       String signedDirectoryDepth, String signedSnapshotTime, String signedEncryptionScope,
138       String rscc, String rscd, String rsce, String rscl, String rsct) {
139     // documentation:
140     // https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas
141     SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
142     df.setTimeZone(TimeZone.getTimeZone("UTC"));
143     List<String> queryArgs = new ArrayList<>();
144     StringBuilder stringBuilder = new StringBuilder();
145     /*
146     StringToSign = signedPermissions + "\n" +
147                signedStart + "\n" +
148                signedExpiry + "\n" +
149                canonicalizedResource + "\n" +
150                signedIdentifier + "\n" +
151                signedIP + "\n" +
152                signedProtocol + "\n" +
153                signedVersion + "\n" +
154                signedResource + "\n" +
155                signedSnapshotTime + "\n" +
156                signedEncryptionScope + "\n" +
157                rscc + "\n" +
158                rscd + "\n" +
159                rsce + "\n" +
160                rscl + "\n" +
161                rsct
162      */
163     //    signedPermissions (sp)
164     stringBuilder.append(StringUtils.trimToEmpty(signedPermissions) + "\n");
165     queryArgs.add("sp=" + StringUtils.trimToEmpty(signedPermissions));
166     //    signedStart (st)
167     Date startDate = signedStart;
168     if (startDate == null) {
169       startDate = Date.from(Instant.now().minus(15, ChronoUnit.MINUTES));
170     }
171     stringBuilder.append(df.format(startDate) + "\n");
172     queryArgs.add("st=" + df.format(startDate));
173     //    signedExpiry (se)
174     Date endDate = signedExpiry;
175     if (endDate == null) {
176       endDate = Date.from(Instant.now().plus(1, ChronoUnit.DAYS));
177     }
178     stringBuilder.append(df.format(endDate) + "\n");
179     queryArgs.add("se=" + df.format(endDate));
180     //    canonicalizedResource ()
181     String canonicalizedResource = Paths.get("/blob", azureStorageAccountName, StringUtils.trimToEmpty(resource))
182         .normalize().toString();
183     if (StringUtils.endsWith(canonicalizedResource,"/")) {
184       canonicalizedResource = StringUtils.substring(canonicalizedResource, 0, canonicalizedResource.length() - 1);
185     }
186     stringBuilder.append(canonicalizedResource + "\n");
187     //    signedIdentifier (si)
188     stringBuilder.append(StringUtils.trimToEmpty(signedIdentifier) + "\n");
189     if (StringUtils.isNotBlank(signedIdentifier)) {
190       queryArgs.add("si=" + StringUtils.trimToEmpty(signedIdentifier));
191     }
192     //    signedIP (sip)
193     stringBuilder.append(StringUtils.trimToEmpty(signedIP) + "\n");
194     if (StringUtils.isNotBlank(signedIP)) {
195       queryArgs.add("sip=" + StringUtils.trimToEmpty(signedIP));
196     }
197     //    signedProtocol (spr)
198     stringBuilder.append("https" + "\n");
199     queryArgs.add("spr=https");
200     //    signedVersion (sv)
201     if (StringUtils.isNotBlank(signedVersion)) {
202       stringBuilder.append(StringUtils.trimToEmpty(signedVersion) + "\n");
203       queryArgs.add("sv=" + StringUtils.trimToEmpty(signedVersion));
204     } else {
205       stringBuilder.append(AZURE_STORAGE_VERSION + "\n");
206       queryArgs.add("sv=" + AZURE_STORAGE_VERSION);
207     }
208     //    signedResource (sr)
209     stringBuilder.append(StringUtils.trimToEmpty(signedResource) + "\n");
210     if (StringUtils.isNotBlank(signedResource)) {
211       queryArgs.add("sr=" + StringUtils.trimToEmpty(signedResource));
212     }
213     //    sr=d -> signedDirectoryDepth (sdd)
214     if (StringUtils.isNotBlank(signedDirectoryDepth)) {
215       queryArgs.add("sdd=" + StringUtils.trimToEmpty(signedDirectoryDepth));
216     }
217     stringBuilder.append(StringUtils.trimToEmpty(signedSnapshotTime) + "\n");
218     //    if (StringUtils.isNotBlank(signedSnapshotTime)) {
219     //      queryArgs.add("sst???=" + StringUtils.trimToEmpty(signedSnapshotTime));
220     //    }
221     //    signedEncryptionScope (ses)
222     stringBuilder.append(StringUtils.trimToEmpty(signedEncryptionScope) + "\n");
223     if (StringUtils.isNotBlank(signedEncryptionScope)) {
224       queryArgs.add("ses=" + StringUtils.trimToEmpty(signedEncryptionScope));
225     }
226     //    rscc = Cache-Control (rscc)
227     stringBuilder.append(StringUtils.trimToEmpty(rscc) + "\n");
228     if (StringUtils.isNotBlank(rscc)) {
229       queryArgs.add("rscc=" + StringUtils.trimToEmpty(rscc));
230     }
231     //    rscd = Content-Disposition (rscd)
232     stringBuilder.append(StringUtils.trimToEmpty(rscd) + "\n");
233     if (StringUtils.isNotBlank(rscd)) {
234       queryArgs.add("rscd=" + StringUtils.trimToEmpty(rscd));
235     }
236     //    rsce = Content-Encoding (rsce)
237     stringBuilder.append(StringUtils.trimToEmpty(rsce) + "\n");
238     if (StringUtils.isNotBlank(rsce)) {
239       queryArgs.add("rsce=" + StringUtils.trimToEmpty(rsce));
240     }
241     //    rscl = Content-Language (rscl)
242     stringBuilder.append(StringUtils.trimToEmpty(rscl) + "\n");
243     if (StringUtils.isNotBlank(rscl)) {
244       queryArgs.add("rscl=" + StringUtils.trimToEmpty(rscl));
245     }
246     //    rsct = Content-Type (rsct)
247     stringBuilder.append(StringUtils.trimToEmpty(rsct));
248     if (StringUtils.isNotBlank(rsct)) {
249       queryArgs.add("rsct=" + StringUtils.trimToEmpty(rsct));
250     }
251     String stringToSign = stringBuilder.toString();
252 
253     Mac initializedMac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256,
254         Base64.decodeBase64(azureAccountAccessKey));
255     byte[] signedString = initializedMac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
256     String signature = Base64.encodeBase64String(signedString);
257     queryArgs.add("sig=" + URLEncoder.encode(signature, StandardCharsets.UTF_8));
258     return StringUtils.joinWith("&", queryArgs.toArray());
259   }
260 }