Skip to content

Commit 3443c14

Browse files
committed
update OpenAPI spec generation to new auth format
The new auth format specifies audiences in the OpenAPI security definitions block, rather than using the x-security vendor extension. To handle this, we now declare one security definition per issuer-audience set combination. The security definition is given the name of the issuer plus a hash of the audience set in hexadecimal.
1 parent b07263b commit 3443c14

10 files changed

Lines changed: 157 additions & 294 deletions

File tree

endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfig.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.google.common.collect.ImmutableListMultimap;
1919
import com.google.common.collect.ImmutableMap;
2020

21+
import com.google.common.collect.ImmutableSet;
22+
import com.google.common.collect.ImmutableSetMultimap;
2123
import java.util.Collection;
2224
import java.util.Objects;
2325

@@ -31,7 +33,7 @@ public class ApiIssuerAudienceConfig {
3133
.addIssuerAudiences(UNSPECIFIED_NAME, UNSPECIFIED_AUDIENCE)
3234
.build();
3335
public static final ApiIssuerAudienceConfig EMPTY = builder().build();
34-
private final ImmutableListMultimap<String, String> issuerAudiences;
36+
private final ImmutableSetMultimap<String, String> issuerAudiences;
3537

3638
private ApiIssuerAudienceConfig(ApiIssuerAudienceConfig.Builder builder) {
3739
this.issuerAudiences = builder.issuerAudiences.build();
@@ -53,6 +55,14 @@ public boolean hasIssuer(String issuer) {
5355
return issuerAudiences.containsKey(issuer);
5456
}
5557

58+
public ImmutableSet<String> getIssuerNames() {
59+
return issuerAudiences.keySet();
60+
}
61+
62+
public ImmutableSet<String> getAudiences(String issuer) {
63+
return issuerAudiences.get(issuer);
64+
}
65+
5666
@Override
5767
public boolean equals(Object o) {
5868
return o != null && o instanceof ApiIssuerAudienceConfig
@@ -69,8 +79,8 @@ public static Builder builder() {
6979
}
7080

7181
public static class Builder {
72-
private final ImmutableListMultimap.Builder<String, String> issuerAudiences =
73-
ImmutableListMultimap.builder();
82+
private final ImmutableSetMultimap.Builder<String, String> issuerAudiences =
83+
ImmutableSetMultimap.builder();
7484

7585
public Builder addIssuerAudiences(String issuer, String... audiences) {
7686
issuerAudiences.putAll(issuer, audiences);

endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerConfigs.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public boolean hasIssuer(String issuer) {
3434
return issuerConfigs.containsKey(issuer);
3535
}
3636

37+
public IssuerConfig getIssuer(String issuer) {
38+
return issuerConfigs.get(issuer);
39+
}
40+
3741
public boolean isSpecified() {
3842
return !this.equals(UNSPECIFIED);
3943
}

endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
import com.google.common.base.CaseFormat;
4141
import com.google.common.base.Converter;
4242
import com.google.common.base.Function;
43+
import com.google.common.base.Joiner;
4344
import com.google.common.collect.FluentIterable;
4445
import com.google.common.collect.ImmutableList;
4546
import com.google.common.collect.ImmutableListMultimap;
4647
import com.google.common.collect.ImmutableMap;
48+
import com.google.common.collect.ImmutableSet;
4749
import com.google.common.collect.Lists;
4850
import com.google.common.collect.Maps;
4951
import com.google.common.reflect.TypeToken;
@@ -93,6 +95,7 @@
9395
* Generates a {@link Swagger} object representing a set of {@link ApiConfig} objects.
9496
*/
9597
public class SwaggerGenerator {
98+
private static final Joiner COMMA_JOINER = Joiner.on(',');
9699
private static final String API_KEY = "api_key";
97100
private static final String API_KEY_PARAM = "key";
98101
private static final String MANAGEMENT_DEFINITIONS_KEY = "x-google-management";
@@ -238,14 +241,6 @@ private void writeApi(ApiKey apiKey, ImmutableList<? extends ApiConfig> apiConfi
238241
// TODO: This may result in duplicate validations in the future if made available online
239242
genCtx.validator.validate(apiConfigs);
240243
for (ApiConfig apiConfig : apiConfigs) {
241-
for (IssuerConfig issuerConfig : apiConfig.getIssuers().asMap().values()) {
242-
addNonConflictingSecurityDefinition(swagger, issuerConfig);
243-
}
244-
List<String> legacyAudiences = apiConfig.getApiClassConfig().getAudiences();
245-
if (legacyAudiences != null && !legacyAudiences.isEmpty()) {
246-
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER);
247-
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT);
248-
}
249244
for (ApiLimitMetricConfig limitMetric : apiConfig.getApiLimitMetrics()) {
250245
addNonConflictingApiLimitMetric(genCtx.limitMetrics, limitMetric);
251246
}
@@ -343,7 +338,7 @@ private void writeApiMethod(
343338
response.setSchema(new RefProperty(schema.name()));
344339
}
345340
operation.response(200, response);
346-
writeAudiences(swagger, methodConfig, genCtx.writeInternal, operation);
341+
writeAuthConfig(swagger, methodConfig, operation);
347342
if (methodConfig.isApiKeyRequired()) {
348343
operation.addSecurity(API_KEY, ImmutableList.<String>of());
349344
Map<String, SecuritySchemeDefinition> definitions = swagger.getSecurityDefinitions();
@@ -355,44 +350,31 @@ private void writeApiMethod(
355350
addDefinedMetricCosts(genCtx.limitMetrics, operation, methodConfig.getMetricCosts());
356351
}
357352

358-
private void writeAudiences(Swagger swagger, ApiMethodConfig methodConfig, boolean writeInternal,
359-
Operation operation) throws ApiConfigException {
353+
private void writeAuthConfig(Swagger swagger, ApiMethodConfig methodConfig, Operation operation)
354+
throws ApiConfigException {
360355
ApiIssuerAudienceConfig issuerAudiences = methodConfig.getIssuerAudiences();
361356
boolean issuerAudiencesIsEmpty = !issuerAudiences.isSpecified() || issuerAudiences.isEmpty();
362357
List<String> legacyAudiences = methodConfig.getAudiences();
363358
boolean legacyAudiencesIsEmpty = legacyAudiences == null || legacyAudiences.isEmpty();
364359
if (issuerAudiencesIsEmpty && legacyAudiencesIsEmpty) {
365360
return;
366361
}
367-
ImmutableMap<String, Collection<String>> audiences = issuerAudiences.asMap();
368-
// For reversability purposes, we can't use helper data structures here. When Swagger reads
369-
// the document back in, it uses primitive data structures.
370-
ImmutableList.Builder<ImmutableMap<String, ImmutableMap<String, List<String>>>> xSecurity =
371-
ImmutableList.builder();
372362
if (!issuerAudiencesIsEmpty) {
373-
for (Map.Entry<String, Collection<String>> entry : audiences.entrySet()) {
374-
operation.addSecurity(entry.getKey(), ImmutableList.<String>of());
375-
if (writeInternal) {
376-
xSecurity.add(ImmutableMap.of(entry.getKey(), createAudiences(entry.getValue())));
377-
}
363+
for (String issuer : issuerAudiences.getIssuerNames()) {
364+
ImmutableSet<String> audiences = issuerAudiences.getAudiences(issuer);
365+
IssuerConfig issuerConfig = methodConfig.getApiConfig().getIssuers().getIssuer(issuer);
366+
String fullIssuer = addNonConflictingSecurityDefinition(swagger, issuerConfig, audiences);
367+
operation.addSecurity(fullIssuer, ImmutableList.<String>of());
378368
}
379369
}
380370
if (!legacyAudiencesIsEmpty) {
381-
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER);
382-
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT);
383-
operation.addSecurity(Constant.GOOGLE_ID_TOKEN_NAME, ImmutableList.<String>of());
384-
operation.addSecurity(Constant.GOOGLE_ID_TOKEN_NAME_HTTPS, ImmutableList.<String>of());
385-
if (writeInternal) {
386-
ImmutableMap<String, List<String>> legacySwaggerAudiences =
387-
createAudiences(legacyAudiences);
388-
xSecurity.add(
389-
ImmutableMap.of(Constant.GOOGLE_ID_TOKEN_NAME, legacySwaggerAudiences));
390-
xSecurity.add(
391-
ImmutableMap.of(Constant.GOOGLE_ID_TOKEN_NAME_HTTPS, legacySwaggerAudiences));
392-
}
393-
}
394-
if (writeInternal) {
395-
operation.setVendorExtension("x-security", xSecurity.build());
371+
ImmutableSet<String> legacyAudienceSet = ImmutableSet.copyOf(legacyAudiences);
372+
String fullIssuer = addNonConflictingSecurityDefinition(
373+
swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER, legacyAudienceSet);
374+
String fullAltIssuer = addNonConflictingSecurityDefinition(
375+
swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT, legacyAudienceSet);
376+
operation.addSecurity(fullIssuer, ImmutableList.<String>of());
377+
operation.addSecurity(fullAltIssuer, ImmutableList.<String>of());
396378
}
397379
}
398380

@@ -484,16 +466,14 @@ private static List<String> getEnumValues(TypeToken<?> t) {
484466
return values;
485467
}
486468

487-
private static ImmutableMap<String, List<String>> createAudiences(Iterable<String> audiences) {
488-
return ImmutableMap.<String, List<String>>of("audiences", ImmutableList.copyOf(audiences));
489-
}
490-
491-
private static SecuritySchemeDefinition toScheme(IssuerConfig issuerConfig) {
469+
private static SecuritySchemeDefinition toScheme(
470+
IssuerConfig issuerConfig, ImmutableSet<String> audiences) {
492471
OAuth2Definition tokenDef = new OAuth2Definition().implicit("");
493472
tokenDef.setVendorExtension("x-google-issuer", issuerConfig.getIssuer());
494473
if (!com.google.common.base.Strings.isNullOrEmpty(issuerConfig.getJwksUri())) {
495474
tokenDef.setVendorExtension("x-google-jwks_uri", issuerConfig.getJwksUri());
496475
}
476+
tokenDef.setVendorExtension("x-google-audiences", COMMA_JOINER.join(audiences));
497477
return tokenDef;
498478
}
499479

@@ -507,17 +487,20 @@ private static Map<String, SecuritySchemeDefinition> getOrCreateSecurityDefiniti
507487
return securityDefinitions;
508488
}
509489

510-
private static void addNonConflictingSecurityDefinition(
511-
Swagger swagger, IssuerConfig issuerConfig) throws ApiConfigException {
490+
private static String addNonConflictingSecurityDefinition(
491+
Swagger swagger, IssuerConfig issuerConfig, ImmutableSet<String> audiences)
492+
throws ApiConfigException {
512493
Map<String, SecuritySchemeDefinition> securityDefinitions =
513494
getOrCreateSecurityDefinitionMap(swagger);
495+
String issuerPlusHash = String.format("%s-%x", issuerConfig.getName(), audiences.hashCode());
514496
SecuritySchemeDefinition existingDef = securityDefinitions.get(issuerConfig.getName());
515-
SecuritySchemeDefinition newDef = toScheme(issuerConfig);
497+
SecuritySchemeDefinition newDef = toScheme(issuerConfig, audiences);
516498
if (existingDef != null && !existingDef.equals(newDef)) {
517499
throw new ApiConfigException(
518500
"Multiple conflicting definitions found for issuer " + issuerConfig.getName());
519501
}
520-
swagger.securityDefinition(issuerConfig.getName(), newDef);
502+
swagger.securityDefinition(issuerPlusHash, newDef);
503+
return issuerPlusHash;
521504
}
522505

523506
public static class SwaggerContext {

endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfigTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import static com.google.common.truth.Truth.assertThat;
44

5-
import com.google.common.collect.ImmutableList;
6-
5+
import com.google.common.collect.ImmutableSet;
76
import org.junit.Test;
87
import org.junit.runner.RunWith;
98
import org.junit.runners.JUnit4;
@@ -31,7 +30,7 @@ public void asMap() {
3130
ApiIssuerAudienceConfig config = ApiIssuerAudienceConfig.builder()
3231
.addIssuerAudiences("issuer", "aud1", "aud2")
3332
.build();
34-
assertThat(config.asMap()).containsExactly("issuer", ImmutableList.of("aud1", "aud2"));
33+
assertThat(config.asMap()).containsExactly("issuer", ImmutableSet.of("aud1", "aud2"));
3534
}
3635

3736
@Test

endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint.swagger

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@
3939
},
4040
"security": [
4141
{
42-
"google_id_token": []
42+
"google_id_token-3a26ea04": []
4343
},
4444
{
45-
"google_id_token_https": []
45+
"google_id_token_https-3a26ea04": []
4646
}
4747
]
4848
},
@@ -59,10 +59,10 @@
5959
},
6060
"security": [
6161
{
62-
"google_id_token": []
62+
"google_id_token-3a26ea04": []
6363
},
6464
{
65-
"google_id_token_https": []
65+
"google_id_token_https-3a26ea04": []
6666
}
6767
]
6868
}
@@ -90,10 +90,10 @@
9090
},
9191
"security": [
9292
{
93-
"google_id_token": []
93+
"google_id_token-3a26ea04": []
9494
},
9595
{
96-
"google_id_token_https": []
96+
"google_id_token_https-3a26ea04": []
9797
}
9898
]
9999
},
@@ -109,8 +109,8 @@
109109
"type": "string"
110110
},
111111
{
112-
"name": "body",
113112
"in": "body",
113+
"name": "body",
114114
"required": false,
115115
"schema": {
116116
"$ref": "#/definitions/Foo"
@@ -127,10 +127,10 @@
127127
},
128128
"security": [
129129
{
130-
"google_id_token": []
130+
"google_id_token-3a26ea04": []
131131
},
132132
{
133-
"google_id_token_https": []
133+
"google_id_token_https-3a26ea04": []
134134
}
135135
]
136136
},
@@ -146,8 +146,8 @@
146146
"type": "string"
147147
},
148148
{
149-
"name": "body",
150149
"in": "body",
150+
"name": "body",
151151
"required": false,
152152
"schema": {
153153
"$ref": "#/definitions/Foo"
@@ -164,10 +164,10 @@
164164
},
165165
"security": [
166166
{
167-
"google_id_token": []
167+
"google_id_token-3a26ea04": []
168168
},
169169
{
170-
"google_id_token_https": []
170+
"google_id_token_https-3a26ea04": []
171171
}
172172
]
173173
},
@@ -193,29 +193,31 @@
193193
},
194194
"security": [
195195
{
196-
"google_id_token": []
196+
"google_id_token-3a26ea04": []
197197
},
198198
{
199-
"google_id_token_https": []
199+
"google_id_token_https-3a26ea04": []
200200
}
201201
]
202202
}
203203
}
204204
},
205205
"securityDefinitions": {
206-
"google_id_token_https": {
206+
"google_id_token-3a26ea04": {
207207
"type": "oauth2",
208208
"authorizationUrl": "",
209209
"flow": "implicit",
210-
"x-google-issuer": "https://accounts.google.com",
211-
"x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs"
210+
"x-google-issuer": "accounts.google.com",
211+
"x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs",
212+
"x-google-audiences": "audience"
212213
},
213-
"google_id_token": {
214+
"google_id_token_https-3a26ea04": {
214215
"type": "oauth2",
215216
"authorizationUrl": "",
216217
"flow": "implicit",
217-
"x-google-issuer": "accounts.google.com",
218-
"x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs"
218+
"x-google-issuer": "https://accounts.google.com",
219+
"x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs",
220+
"x-google-audiences": "audience"
219221
}
220222
},
221223
"definitions": {

0 commit comments

Comments
 (0)