Skip to content

Accept @authzPolicy directive #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import static com.intuit.graphql.orchestrator.resolverdirective.FieldResolverDirectiveUtil.RESOLVER_ARGUMENT_INPUT_NAME;
import static com.intuit.graphql.orchestrator.utils.FederationConstants.FEDERATION_EXTENDS_DIRECTIVE;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkFieldsCompatibility;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkInputObjectTypeCompatibility;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isEntity;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInaccessible;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInputObjectType;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isScalarType;
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.toDescriptiveString;
import static com.intuit.graphql.orchestrator.utils.XtextUtils.definitionContainsDirective;

import com.intuit.graphql.graphQL.EnumTypeDefinition;
import com.intuit.graphql.graphQL.InputObjectTypeDefinition;
import com.intuit.graphql.graphQL.InterfaceTypeDefinition;
import com.intuit.graphql.graphQL.ObjectTypeDefinition;
import com.intuit.graphql.graphQL.TypeDefinition;
Expand Down Expand Up @@ -38,6 +41,13 @@ public void resolve(final TypeDefinition conflictingType, final TypeDefinition e
}

private void checkSameType(final TypeDefinition conflictingType, final TypeDefinition existingType) {
boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType);
if (isInputObjectTypeComparison) {
// need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers
checkInputObjectTypeCompatibility(existingType, conflictingType);
return;
}

if (!(isSameType(conflictingType, existingType) && isScalarType(conflictingType))) {
throw new TypeConflictException(
String.format("Type %s is conflicting with existing type %s", toDescriptiveString(conflictingType),
Expand All @@ -52,6 +62,7 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef
boolean entityComparison = conflictingTypeisEntity && existingTypeIsEntity;
boolean isInaccessibleComparison = isInaccessible(conflictingType) || isInaccessible(existingType);
boolean baseExtensionComparison = definitionContainsDirective(existingType, FEDERATION_EXTENDS_DIRECTIVE) || definitionContainsDirective(conflictingType, FEDERATION_EXTENDS_DIRECTIVE);
boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType);

if(!isInaccessibleComparison) {
if(isEntity(conflictingType) != isEntity(existingType)) {
Expand All @@ -67,6 +78,13 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef
toDescriptiveString(existingType)));
}

if (isInputObjectTypeComparison) {
// need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers
checkInputObjectTypeCompatibility(existingType, conflictingType);
return;

}

if(!(conflictingType instanceof UnionTypeDefinition || isScalarType(conflictingType) || conflictingType instanceof EnumTypeDefinition)) {
checkFieldsCompatibility(existingType, conflictingType, existingTypeIsEntity, conflictingTypeisEntity,federatedComparison);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,58 @@ public static String toDescriptiveString(ArgumentsDefinition argumentsDefinition
return StringUtils.EMPTY;
}

public static void checkInputObjectTypeCompatibility(TypeDefinition existingType, TypeDefinition incomingType) {
if (!(existingType instanceof InputObjectTypeDefinition)) {
throw new TypeConflictException(
format("Type %s is conflicting with Input Type %s. Both types must be of the same type",
toDescriptiveString(existingType),
toDescriptiveString(incomingType)
)
);
}

if (!(incomingType instanceof InputObjectTypeDefinition)) {
throw new TypeConflictException(
format("Type %s is conflicting with Input Type %s. Both types must be of the same type",
toDescriptiveString(incomingType),
toDescriptiveString(existingType)
)
);
}


InputObjectTypeDefinition existingTypeDefinition = (InputObjectTypeDefinition) existingType;
InputObjectTypeDefinition incomingTypeDefinition = (InputObjectTypeDefinition) incomingType;

if (existingTypeDefinition.getInputValueDefinition().size() != incomingTypeDefinition.getInputValueDefinition().size()) {
throw new TypeConflictException(
format("Type %s is conflicting with Input Type %s. Both types must be of the same size",
toDescriptiveString(incomingType),
toDescriptiveString(existingType)
)
);
}

incomingTypeDefinition.getInputValueDefinition()
.forEach(incomingInputValueDefinition -> {
boolean found = existingTypeDefinition.getInputValueDefinition()
.stream()
.anyMatch(existingTnputValueDefinition -> StringUtils.equals(incomingInputValueDefinition.getName(),
existingTnputValueDefinition.getName()));

if (!found) {
throw new TypeConflictException(
format("Type %s is conflicting with Input Type %s. Both types much have the same InputValueDefinition",
toDescriptiveString(incomingType),
toDescriptiveString(existingType)
)
);
}

});
//
}

public static void checkFieldsCompatibility(final TypeDefinition existingTypeDefinition, final TypeDefinition conflictingTypeDefinition,
boolean existingTypeIsEntity, boolean conflictingTypeisEntity, boolean federatedComparison) {
List<FieldDefinition> existingFieldDefinitions = getFieldDefinitions(existingTypeDefinition);
Expand Down Expand Up @@ -291,6 +343,10 @@ public static boolean isValidInputType(NamedType namedType) {
return true;
}

public static boolean isInputObjectType(TypeDefinition typeDefinition) {
return typeDefinition instanceof InputObjectTypeDefinition;
}

public static boolean isEntity(final TypeDefinition type) {
return definitionContainsDirective(type, FEDERATION_KEY_DIRECTIVE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public class XtextResourceSetBuilder {
private Map<String, String> files = new ConcurrentHashMap<>();
private boolean isFederatedResourceSet = false;

public static final String FEDERATION_DIRECTIVES = getFederationDirectives();
public static final String FEDERATION_DIRECTIVES = getDirectiveDefinitions("federation_built_in_directives.graphqls");
public static final String AUTHZPOLICY_DIRECTIVES = getDirectiveDefinitions("authzpolicy_directive_definition.graphqls");

private XtextResourceSetBuilder() {
}
Expand Down Expand Up @@ -79,13 +80,17 @@ public XtextResourceSet build() {

if(isFederatedResourceSet) {
String content = FEDERATION_DIRECTIVES + "\n" + StringUtils.join(files.values(), "\n");

content = addAuthzPolicyDirectiveDefinition(content);
try {
createGraphqlResourceFromString(content, "appended_federation");
} catch (IOException e) {
throw new SchemaParseException("Unable to parse file: appended federation file", e);
}
} else {
if (isUsingAuthzPolicy(files)) {
files.put("authzpolicy_directive_definition.graphqls", AUTHZPOLICY_DIRECTIVES);
}

files.forEach((fileName, content) -> {
try {
createGraphqlResourceFromString(content, fileName);
Expand All @@ -102,6 +107,18 @@ public XtextResourceSet build() {
return graphqlResourceSet;
}

private boolean isUsingAuthzPolicy(Map<String, String> files) {
return files.values().stream()
.anyMatch(content -> StringUtils.contains(content, "@authzPolicy"));
}

public String addAuthzPolicyDirectiveDefinition(String content) {
if (StringUtils.contains(content, "@authzPolicy")) {
return content + "\n" + AUTHZPOLICY_DIRECTIVES;
}
return content;
}

private XtextResource createResourceFrom(InputStream input, URI uri, Injector injector) throws IOException {
XtextResource resource = (XtextResource) (injector.getInstance(IResourceFactory.class).createResource(uri));
resource.load(input, null);
Expand Down Expand Up @@ -143,12 +160,12 @@ public static XtextResourceSet singletonSet(String fileName, String file) {
.build();
}

private static String getFederationDirectives() {
private static String getDirectiveDefinitions(String file) {
String directives = "";
try {
directives = IOUtils.toString(
XtextResourceSetBuilder.class.getClassLoader()
.getResourceAsStream( "federation_built_in_directives.graphqls"),
.getResourceAsStream( file),
Charset.defaultCharset()
);
} catch (IOException ex) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/authzpolicy_directive_definition.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
directive @authzPolicy(id: String, ruleInputs:[RuleInput]!) on FIELD_DEFINITION
input RuleInput { key: String! value: [String]!}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.intuit.graphql.orchestrator.integration.authzpolicy

import com.intuit.graphql.orchestrator.ServiceProvider
import com.intuit.graphql.orchestrator.datafetcher.ServiceDataFetcher
import com.intuit.graphql.orchestrator.schema.Operation
import com.intuit.graphql.orchestrator.stitching.StitchingException
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLCodeRegistry
import graphql.schema.GraphQLSchema
import graphql.schema.StaticDataFetcher
import helpers.BaseIntegrationTestSpecification
import spock.lang.Subject

import static com.intuit.graphql.orchestrator.TestHelper.getResourceAsString
import static graphql.Scalars.GraphQLInt

class AuthzPolicyWithNestedLevelStitchingSpec extends BaseIntegrationTestSpecification {

def v4osService, turboService

def mockServiceResponse = new HashMap()

def conflictingSchema = """
type Query {
root: RootType
topLevelField: NestedType @authzPolicy(ruleInput: [{key:"foo",value:"a"}])
}

type RootType {
nestedField: NestedType
}

type NestedType {
fieldA: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
fieldB: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
}
"""

def conflictingSchema2 = """
type Query {
root: RootType
}

type RootType {
nestedField: NestedType
}

type NestedType {
fieldC: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
fieldD: String @authzPolicy(ruleInput: [{key:"foo",value:"c"}])
}
"""
ServiceProvider topLevelDeprecatedService1
ServiceProvider topLevelDeprecatedService2


@Subject
def specUnderTest

def "Test Nested Stitching"() {
given:
v4osService = createSimpleMockService("V4OS", getResourceAsString("nested/v4os/schema.graphqls"), mockServiceResponse)
turboService = createSimpleMockService("TURBO", getResourceAsString("nested/turbo/schema.graphqls"), mockServiceResponse)

when:
specUnderTest = createGraphQLOrchestrator([v4osService, turboService])

then:
GraphQLSchema result = specUnderTest.schema

def consumer = result?.getQueryType()?.getFieldDefinition("consumer")
consumer != null

def consumerType = result?.getQueryType()?.getFieldDefinition("consumer")?.type
consumerType != null
consumerType.name == "ConsumerType"
consumerType.description == "[V4OS,TURBO]"

def financialProfile = consumerType?.getFieldDefinition("financialProfile")
financialProfile != null
financialProfile.type?.name == "FinancialProfileType"

def turboExperiences = consumerType?.getFieldDefinition("turboExperiences")
turboExperiences != null
turboExperiences.type?.name == "ExperienceType"

def financeField = consumerType?.getFieldDefinition("finance")
financeField != null
financeField.type?.name == "FinanceType"
financeField.type?.getFieldDefinition("fieldFinance")?.type == GraphQLInt
financeField.type?.getFieldDefinition("fieldTurbo")?.type == GraphQLInt

//DataFetchers
final GraphQLCodeRegistry codeRegistry = specUnderTest.runtimeGraph.getCodeRegistry().build()
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("Query", "consumer"), consumer) instanceof StaticDataFetcher
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "finance"), financeField) instanceof StaticDataFetcher
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "financialProfile"), financeField) instanceof ServiceDataFetcher
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "turboExperiences"), turboExperiences) instanceof ServiceDataFetcher
}

def "Nested Type Description With Namespace And Empty Description"() {
given:
def bSchema = "schema { query: Query } type Query { a: A } \"\n" +\
" \"type A { b: B @adapter(service: 'foo') } type B {d: D}\"\n" +\
" \"type D { field: String}\"\n" +\
" \"directive @adapter(service:String!) on FIELD_DEFINITION"

def bbSchema = "schema { query: Query } type Query { a: A } \"\n" +\
" \"type A { bbc: BB } type BB {cc: String}"

def abcSchema = "schema { query: Query } type Query { a: A } \"\n" +\
" \"type A { bbcd: C } type C {cc: String}"

def secondSchema = "schema { query: Query } type Query { a: A } " +\
"type A { bbbb: BAB } type BAB {fieldBB: String}"

def ambcSchema = "schema { query: Query } type Query { a: A } \"\n" +\
" \"type A { bba: CDD } type CDD {ccdd: String}"

def ttbbSchema = "schema { query: Query } type Query { a: A } \"\n" +\
" \"type A { bbab: BBD } type BBD {cc: String}"

def bService = createSimpleMockService("SVC_b", bSchema, mockServiceResponse)
def bbService = createSimpleMockService("SVC_bb", bbSchema, mockServiceResponse)
def abcService = createSimpleMockService("SVC_abc", abcSchema, mockServiceResponse)
def secondService = createSimpleMockService("SVC_Second", secondSchema, mockServiceResponse)
def ambcService = createSimpleMockService("AMBC", ambcSchema, mockServiceResponse)
def ttbbService = createSimpleMockService("TTBB", ttbbSchema, mockServiceResponse)

when:
specUnderTest = createGraphQLOrchestrator([bService, bbService ,abcService,secondService, ambcService, ttbbService])


then:
def aType = specUnderTest.runtimeGraph.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type

aType.description.contains("SVC_abc")
aType.description.contains("SVC_bb")
aType.description.contains("AMBC")
aType.description.contains("TTBB")
aType.description.contains("SVC_Second")
aType.description.contains("SVC_b")
}

def "Nested Type Description With Namespace And Description"() {
given:
String schema1 = "schema { query: Query } type Query { a: A } " +\
"type A { b: B @adapter(service: 'foo') } type B {d: D}" +\
"type D { field: String}" +\
"directive @adapter(service:String!) on FIELD_DEFINITION"

String schema2 = "schema { query: Query } type Query { a: A } " +\
"\"description for schema2\"type A { bbc: BB } type BB {cc: String}"

String schema3 = "schema { query: Query } type Query { a: A } " +\
"\"description for schema3\"type A { bbcd: C } type C {cc: String}"

String schema4 = "schema { query: Query } type Query { a: A } " +\
"type A { bbbb: BAB } type BAB {fieldBB: String}"

def service1 = createSimpleMockService("SVC_b", schema1, mockServiceResponse)
def service2 = createSimpleMockService("SVC_bb", schema2, mockServiceResponse)
def service3 = createSimpleMockService("SVC_abc", schema3, mockServiceResponse)
def service4 = createSimpleMockService("SVC_Second", schema4, mockServiceResponse)

when:
specUnderTest = createGraphQLOrchestrator([service1, service2, service3, service4])

then:
def aType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type

aType.description.contains("SVC_abc")
aType.description.contains("SVC_bb")
aType.description.contains("SVC_Second")
aType.description.contains("SVC_b")
aType.description.contains("description for schema3")
aType.description.contains("description for schema2")
}

def "deprecated fields can not be referenced again"() {
given:
topLevelDeprecatedService1 = createSimpleMockService("test1", conflictingSchema, new HashMap<String, Object>())
topLevelDeprecatedService2 = createSimpleMockService("test2", conflictingSchema2, new HashMap<String, Object>())

when:
specUnderTest = createGraphQLOrchestrator([topLevelDeprecatedService1, topLevelDeprecatedService2])

then:
def exception = thrown(StitchingException)
exception.message == "FORBIDDEN: Subgraphs [test2,test1] are reusing type NestedType with different field definitions."
}
}
Loading
Loading