-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move some CLI output parsing to stream style (#39008)
* Move some parsing to stream style I noticed a bug in how parsing `az` output was working after adding support for the new `expires_on` field. The path to fixing that required fixing how parsing the json worked, so I took the opportunity to start moving to stream-style deserialization. * revert some string updates and fix a variable name that failed checkstyle * fix test failure, improve parsing. * PR feedback
- Loading branch information
Showing
6 changed files
with
253 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
.../azure-identity/src/main/java/com/azure/identity/implementation/models/AzureCliToken.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.azure.identity.implementation.models; | ||
|
||
import com.azure.json.JsonReader; | ||
import com.azure.json.JsonSerializable; | ||
import com.azure.json.JsonToken; | ||
import com.azure.json.JsonWriter; | ||
|
||
import java.io.IOException; | ||
import java.time.Instant; | ||
import java.time.LocalDateTime; | ||
import java.time.OffsetDateTime; | ||
import java.time.ZoneId; | ||
import java.time.ZoneOffset; | ||
import java.time.format.DateTimeFormatter; | ||
|
||
/** | ||
* A wrapper class for deserializing a token payload returned from the Azure CLI. | ||
*/ | ||
public final class AzureCliToken implements JsonSerializable<AzureCliToken> { | ||
private String accessToken; | ||
private String expiresOn; | ||
private Long expiresOnUnixTime; | ||
private String subscription; | ||
private String tenant; | ||
private String tokenType; | ||
private OffsetDateTime tokenExpiry; | ||
|
||
public String getAccessToken() { | ||
return accessToken; | ||
} | ||
|
||
public String getExpiresOn() { | ||
return expiresOn; | ||
} | ||
|
||
public Long getExpiresOnUnixTime() { | ||
return expiresOnUnixTime; | ||
} | ||
|
||
public String getSubscription() { | ||
return subscription; | ||
} | ||
|
||
public String getTenant() { | ||
return tenant; | ||
} | ||
|
||
public String getTokenType() { | ||
return tokenType; | ||
} | ||
|
||
public OffsetDateTime getTokenExpiration() { | ||
return tokenExpiry; | ||
} | ||
|
||
private static OffsetDateTime parseExpiresOnTime(String time) { | ||
OffsetDateTime tokenExpiry; | ||
// parse the incoming date: 2024-02-28 12:05:53.000000 | ||
tokenExpiry = LocalDateTime.parse(time, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")) | ||
.atZone(ZoneId.systemDefault()) | ||
.toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC); | ||
return tokenExpiry; | ||
} | ||
|
||
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { | ||
jsonWriter.writeStartObject(); | ||
jsonWriter.writeStringField("accessToken", accessToken); | ||
jsonWriter.writeStringField("expiresOn", expiresOn); | ||
jsonWriter.writeNumberField("expires_on", expiresOnUnixTime); | ||
jsonWriter.writeStringField("subscription", subscription); | ||
jsonWriter.writeStringField("tenant", tenant); | ||
jsonWriter.writeStringField("tokenType", tokenType); | ||
jsonWriter.writeEndObject(); | ||
return jsonWriter; | ||
} | ||
|
||
public static AzureCliToken fromJson(JsonReader jsonReader) throws IOException { | ||
return jsonReader.readObject(reader -> { | ||
AzureCliToken tokenHolder = new AzureCliToken(); | ||
while (reader.nextToken() != JsonToken.END_OBJECT) { | ||
String fieldName = reader.getFieldName(); | ||
reader.nextToken(); | ||
if ("accessToken".equals(fieldName)) { | ||
tokenHolder.accessToken = reader.getString(); | ||
} else if ("expiresOn".equals(fieldName)) { | ||
tokenHolder.expiresOn = reader.getString(); | ||
} else if ("expires_on".equals(fieldName)) { | ||
tokenHolder.expiresOnUnixTime = reader.getLong(); | ||
} else if ("subscription".equals(fieldName)) { | ||
tokenHolder.subscription = reader.getString(); | ||
} else if ("tenant".equals(fieldName)) { | ||
tokenHolder.tenant = reader.getString(); | ||
} else if ("tokenType".equals(fieldName)) { | ||
tokenHolder.tokenType = reader.getString(); | ||
} else { | ||
reader.skipChildren(); | ||
} | ||
} | ||
|
||
if (tokenHolder.expiresOnUnixTime != null) { | ||
tokenHolder.tokenExpiry = Instant.ofEpochSecond(tokenHolder.getExpiresOnUnixTime()).atOffset(ZoneOffset.UTC); | ||
} else { | ||
tokenHolder.tokenExpiry = parseExpiresOnTime(tokenHolder.getExpiresOn()); | ||
} | ||
|
||
return tokenHolder; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
...e-identity/src/test/java/com/azure/identity/implementation/models/AzureCliTokenTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.azure.identity.implementation.models; | ||
|
||
import com.azure.json.JsonProviders; | ||
import com.azure.json.JsonReader; | ||
import com.azure.json.JsonWriter; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.time.Clock; | ||
import java.time.Instant; | ||
import java.time.LocalDateTime; | ||
import java.time.OffsetDateTime; | ||
import java.time.ZoneId; | ||
import java.time.format.DateTimeFormatter; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNull; | ||
|
||
public class AzureCliTokenTests { | ||
|
||
String jsonWithExpiresOnUnixTime = "{\n" | ||
+ " \"accessToken\": \"tokenValue\",\n" | ||
+ " \"expiresOn\": \"2024-02-28 12:05:53.000000\",\n" | ||
+ " \"expires_on\": 1709150753,\n" | ||
+ " \"subscription\": \"subscriptionValue\",\n" | ||
+ " \"tenant\": \"tenantValue\",\n" | ||
+ " \"tokenType\": \"Bearer\"\n" | ||
+ "}"; | ||
|
||
// This is the payload that gets parsed in the fallback case. It does not have time zone information. | ||
// For test purposes, we need to inject the current time here, so the test works in different regions. | ||
String jsonWithoutExpiresOnUnixTime = "{\n" | ||
+ " \"accessToken\": \"tokenValue\",\n" | ||
+ " \"expiresOn\": \"%s\",\n" | ||
+ " \"subscription\": \"subscriptionValue\",\n" | ||
+ " \"tenant\": \"tenantValue\",\n" | ||
+ " \"tokenType\": \"Bearer\"\n" | ||
+ "}"; | ||
@Test | ||
public void testRoundTripWithoutExpiresOnUnixTime() { | ||
ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||
// This test is largely testing the round trip and conversion of the local time returned from az | ||
// to a UTC time. Set up the current time to allow this to work in all time zones. | ||
Clock clock = Clock.fixed(Instant.parse("2024-02-28T20:05:53.123456Z"), ZoneId.of("Z")); | ||
OffsetDateTime expected = OffsetDateTime.now(clock); | ||
LocalDateTime localNow = expected.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
expected = expected.atZoneSameInstant(ZoneId.of("Z")).toOffsetDateTime(); | ||
// recreate the incorrect date format from az | ||
String nowString = localNow.format(DateTimeFormatter.ISO_DATE) + " " + localNow.format(DateTimeFormatter.ISO_TIME); | ||
String localJson = String.format(jsonWithoutExpiresOnUnixTime, nowString); | ||
|
||
try { | ||
try (JsonReader reader = JsonProviders.createReader(localJson)) { | ||
AzureCliToken token = AzureCliToken.fromJson(reader); | ||
JsonWriter writer = JsonProviders.createWriter(stream); | ||
token.toJson(writer); | ||
assertNull(token.getExpiresOnUnixTime()); | ||
assertEquals("tokenValue", token.getAccessToken()); | ||
assertEquals(nowString, token.getExpiresOn()); | ||
assertEquals("subscriptionValue", token.getSubscription()); | ||
assertEquals("tenantValue", token.getTenant()); | ||
assertEquals("Bearer", token.getTokenType()); | ||
assertEquals(expected, token.getTokenExpiration()); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
@Test | ||
public void testRoundTripWithExpiresOnUnixTime() { | ||
ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||
|
||
try { | ||
try (JsonReader reader = JsonProviders.createReader(jsonWithExpiresOnUnixTime)) { | ||
AzureCliToken token = AzureCliToken.fromJson(reader); | ||
JsonWriter writer = JsonProviders.createWriter(stream); | ||
token.toJson(writer); | ||
assertEquals("tokenValue", token.getAccessToken()); | ||
assertEquals("2024-02-28 12:05:53.000000", token.getExpiresOn()); | ||
assertEquals(1709150753, token.getExpiresOnUnixTime()); | ||
assertEquals("subscriptionValue", token.getSubscription()); | ||
assertEquals("tenantValue", token.getTenant()); | ||
assertEquals("Bearer", token.getTokenType()); | ||
// this test works fine with the hardcoded values since we don't care about the conversion through | ||
// local time. | ||
assertEquals(OffsetDateTime.parse("2024-02-28T20:05:53Z"), token.getTokenExpiration()); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
// This test validates the json parsing works both ways. | ||
@Test | ||
public void testDoubleRoundTrip() { | ||
ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||
|
||
try (JsonReader reader = JsonProviders.createReader(jsonWithExpiresOnUnixTime)) { | ||
AzureCliToken token = AzureCliToken.fromJson(reader); | ||
JsonWriter writer = JsonProviders.createWriter(stream); | ||
token.toJson(writer); | ||
writer.close(); | ||
|
||
try (JsonReader reader2 = JsonProviders.createReader(stream.toByteArray())) { | ||
AzureCliToken token2 = AzureCliToken.fromJson(reader2); | ||
assertEquals("tokenValue", token2.getAccessToken()); | ||
assertEquals("2024-02-28 12:05:53.000000", token2.getExpiresOn()); | ||
assertEquals(1709150753, token2.getExpiresOnUnixTime()); | ||
assertEquals("subscriptionValue", token2.getSubscription()); | ||
assertEquals("tenantValue", token2.getTenant()); | ||
assertEquals("Bearer", token2.getTokenType()); | ||
assertEquals(OffsetDateTime.parse("2024-02-28T20:05:53Z"), token2.getTokenExpiration()); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |