Skip to content

Feature/upgrading version 2.27.4 #3076

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions aws-api-appsync/api/aws-api-appsync.api
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public final class com/amplifyframework/api/aws/AppSyncGraphQLRequest : com/ampl
public fun getModelSchema ()Lcom/amplifyframework/core/model/ModelSchema;
public fun getOperation ()Lcom/amplifyframework/api/graphql/Operation;
public fun getQuery ()Ljava/lang/String;
public fun getSelectionSet ()Lcom/amplifyframework/api/aws/SelectionSet;
public fun getVariables ()Ljava/util/Map;
public fun hashCode ()I
public fun newBuilder ()Lcom/amplifyframework/api/aws/AppSyncGraphQLRequest$Builder;
Expand Down Expand Up @@ -92,11 +93,16 @@ public final class com/amplifyframework/api/aws/SelectionSet {
public static fun builder ()Lcom/amplifyframework/api/aws/SelectionSet$Builder;
public fun equals (Ljava/lang/Object;)Z
public fun getNodes ()Ljava/util/Set;
public fun getValue ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun toString (Ljava/lang/String;)Ljava/lang/String;
}

public final class com/amplifyframework/api/aws/SelectionSetUtils {
public static final fun findChildByName (Lcom/amplifyframework/api/aws/SelectionSet;Ljava/lang/String;)Lcom/amplifyframework/api/aws/SelectionSet;
}

public final class com/amplifyframework/api/graphql/GsonResponseAdapters {
public static fun register (Lcom/google/gson/GsonBuilder;)V
}
Expand Down
1 change: 0 additions & 1 deletion aws-api-appsync/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ plugins {
id("kotlin-android")
}

apply(from = rootProject.file("configuration/checkstyle.gradle"))
apply(from = rootProject.file("configuration/publishing.gradle"))

group = properties["POM_GROUP"].toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
public final class AppSyncGraphQLRequest<R> extends GraphQLRequest<R> {
private final ModelSchema modelSchema;
private final Operation operation;

public SelectionSet getSelectionSet() {
return selectionSet;
}

private final SelectionSet selectionSet;
private final Map<String, Object> variables;
private final Map<String, String> variableTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public SelectionSet(String value, @NonNull Set<SelectionSet> nodes) {
* @return node value
*/
@Nullable
protected String getValue() {
public String getValue() {
return value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.amplifyframework.core.model.PropertyContainerPath
* @param name: the name to match the child node of type `SelectionSetField`
* @return the matched `SelectionSet` or `nil` if there's no child with the specified name.
*/
internal fun SelectionSet.findChildByName(name: String) = nodes.find { it.value == name }
fun SelectionSet.findChildByName(name: String) = nodes.find { it.value == name }

/**
* Replaces or adds a new child to the selection set tree. When a child node exists
Expand Down
1 change: 1 addition & 0 deletions aws-api/api/aws-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public final class com/amplifyframework/api/aws/auth/ApiRequestDecoratorFactory
public final class com/amplifyframework/api/aws/auth/AuthRuleRequestDecorator {
public fun <init> (Lcom/amplifyframework/api/aws/ApiAuthProviders;)V
public fun decorate (Lcom/amplifyframework/api/graphql/GraphQLRequest;Lcom/amplifyframework/api/aws/AuthorizationType;)Lcom/amplifyframework/api/graphql/GraphQLRequest;
public static fun filterAuthRules (Ljava/util/List;Lcom/amplifyframework/core/model/ModelSchema;)Ljava/util/List;
}

public final class com/amplifyframework/api/aws/auth/CognitoJWTParser {
Expand Down
1 change: 0 additions & 1 deletion aws-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ plugins {
id("kotlin-android")
}

apply(from = rootProject.file("configuration/checkstyle.gradle"))
apply(from = rootProject.file("configuration/publishing.gradle"))

group = properties["POM_GROUP"].toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@
import com.amplifyframework.api.aws.ApiAuthProviders;
import com.amplifyframework.api.aws.AppSyncGraphQLRequest;
import com.amplifyframework.api.aws.AuthorizationType;
import com.amplifyframework.api.aws.SelectionSet;
import com.amplifyframework.api.aws.SelectionSetUtils;
import com.amplifyframework.api.aws.sigv4.CognitoUserPoolsAuthProvider;
import com.amplifyframework.api.aws.sigv4.DefaultCognitoUserPoolsAuthProvider;
import com.amplifyframework.api.aws.sigv4.OidcAuthProvider;
import com.amplifyframework.api.graphql.GraphQLRequest;
import com.amplifyframework.api.graphql.OperationType;
import com.amplifyframework.core.model.AuthRule;
import com.amplifyframework.core.model.AuthStrategy;
import com.amplifyframework.core.model.ModelField;
import com.amplifyframework.core.model.ModelOperation;
import com.amplifyframework.core.model.ModelSchema;

import org.json.JSONArray;
import org.json.JSONException;
Expand All @@ -53,24 +58,38 @@ public final class AuthRuleRequestDecorator {

/**
* Constructs a new instance of GraphQL request's auth rule processor.
*
* @param authProvider the auth providers to authorize requests
*/
public AuthRuleRequestDecorator(@NonNull ApiAuthProviders authProvider) {
this.authProvider = Objects.requireNonNull(authProvider);
}

public static List<AuthRule> filterAuthRules(List<AuthRule> authRules, ModelSchema modelSchema) {
List<AuthRule> result = new ArrayList<>();

for (AuthRule authRule : authRules) {
ModelField modelField = modelSchema.getFields().get(authRule.getOwnerFieldOrDefault());
if (modelField == null || Objects.equals(modelField.getJavaClassForValue(), String.class)) {
result.add(authRule);
}
}
return result;
}

/**
* Decorate given GraphQL request instance with additional variables for owner-based or
* group-based authorization.
*
* <p>
* This will only work if the request is compliant with the AppSync specifications.
* @param request an instance of {@link GraphQLRequest}.
*
* @param request an instance of {@link GraphQLRequest}.
* @param authType the mode of authorization being used to authorize the request
* @param <R> The type of data contained in the GraphQLResponse expected from this request.
* @param <R> The type of data contained in the GraphQLResponse expected from this request.
* @return the input request with additional variables that specify model's owner and/or
* groups
* groups
* @throws ApiException If an error is encountered while processing the auth rules associated
* with the request or if the authorization fails
* with the request or if the authorization fails
*/
public <R> GraphQLRequest<R> decorate(
@NonNull GraphQLRequest<R> request,
Expand All @@ -80,30 +99,28 @@ public <R> GraphQLRequest<R> decorate(
return request;
}

AppSyncGraphQLRequest<R> appSyncRequest = (AppSyncGraphQLRequest<R>) request;
AuthRule ownerRuleWithReadRestriction = null;
AppSyncGraphQLRequest<R> decoratedReqeust = (AppSyncGraphQLRequest<R>) request;
List<AuthRule> authRules = filterAuthRules(decoratedReqeust.getModelSchema().getAuthRules(), decoratedReqeust.getModelSchema());
List<AuthRule> ownerRulesListWithReadRestriction = new ArrayList<>();
Map<String, Set<String>> readAuthorizedGroupsMap = new HashMap<>();
boolean publicSubscribeAllowed = false;

if (authRules.isEmpty())
return request;


boolean publicSubscribeAllowed = false;
// Note that we are intentionally supporting only one owner rule with a READ operation at this time.
// If there is more than one, the operation will fail because AppSync generates a parameter for each
// one. The question then is which one do we pass. JavaScript currently doesn't support this use case
// and it's not clear what a good solution would be until AppSync supports real time filters.
for (AuthRule authRule : appSyncRequest.getModelSchema().getAuthRules()) {
for (AuthRule authRule : authRules) {
if (doesRuleAllowPublicSubscribe(authRule, authType)) {
// This rule allows subscribing with the current authMode without adding the owner field, so there
// is no need to continue checking the other rules.
publicSubscribeAllowed = true;
break;
} else if (isReadRestrictingOwner(authRule)) {
if (ownerRuleWithReadRestriction == null) {
ownerRuleWithReadRestriction = authRule;
} else {
throw new ApiAuthException(
"Detected multiple owner type auth rules with a READ operation",
"We currently do not support this use case. Please limit your type to just one owner " +
"auth rule with a READ operation restriction.");
}
ownerRulesListWithReadRestriction.add(authRule);
} else if (isReadRestrictingStaticGroup(authRule)) {
// Group read-restricting groups by the claim name
String groupClaim = authRule.getGroupClaimOrDefault();
Expand All @@ -120,36 +137,55 @@ public <R> GraphQLRequest<R> decorate(
// We only add the owner parameter to the subscription if there is an owner rule with a READ restriction
// and either there are no group auth rules with read access or there are but the user isn't in any of
// them.
if (!publicSubscribeAllowed &&
ownerRuleWithReadRestriction != null
&& userNotInReadRestrictingGroups(readAuthorizedGroupsMap, authType)) {
String idClaim = ownerRuleWithReadRestriction.getIdentityClaimOrDefault();
String key = ownerRuleWithReadRestriction.getOwnerFieldOrDefault();
String value = getIdentityValue(idClaim, authType);

try {
return appSyncRequest.newBuilder()
.variable(key, "String!", value)
.build();
} catch (AmplifyException error) {
// This should not happen normally
throw new ApiAuthException(
"Failed to set owner field on AppSyncGraphQLRequest.", error,
AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION);
for (AuthRule ownerBasedRule : ownerRulesListWithReadRestriction) {
String owner = ownerBasedRule.getOwnerFieldOrDefault();
SelectionSet selectionSet = appendOwnersIfNeeded(decoratedReqeust.getSelectionSet(), owner);
if (decoratedReqeust.getOperation().getOperationType() == OperationType.SUBSCRIPTION) {
if (!publicSubscribeAllowed
&& userNotInReadRestrictingGroups(readAuthorizedGroupsMap, authType)) {

String idClaim = ownerBasedRule.getIdentityClaimOrDefault();
String value = getIdentityValue(idClaim, authType);

try {
decoratedReqeust = decoratedReqeust.newBuilder()
.variable(owner, "String!", value)
.selectionSet(selectionSet)
.build();
} catch (AmplifyException error) {
// This should not happen normally
throw new ApiAuthException(
"Failed to set owner field on AppSyncGraphQLRequest.", error,
AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION);
}
}
}
}

return request;
return decoratedReqeust;
}

private SelectionSet appendOwnersIfNeeded(SelectionSet originalSelection, String ownerField) {
SelectionSet resultSelection = originalSelection;

boolean hasOwnerField = SelectionSetUtils.findChildByName(resultSelection, ownerField) != null;

if (!hasOwnerField) {
resultSelection.getNodes().add(new SelectionSet(ownerField));
}

return resultSelection;
}

private boolean doesRuleAllowPublicSubscribe(AuthRule authRule, AuthorizationType authMode) {
AuthorizationType typeForRule = AuthorizationType.from(authRule.getAuthProvider());
AuthStrategy strategy = authRule.getAuthStrategy();
List<ModelOperation> operations = authRule.getOperationsOrDefault();
return strategy == AuthStrategy.PUBLIC
&& typeForRule == AuthorizationType.API_KEY
&& authMode == AuthorizationType.API_KEY
&& (operations.contains(ModelOperation.LISTEN) || operations.contains(ModelOperation.READ));
&& typeForRule == AuthorizationType.API_KEY
&& authMode == AuthorizationType.API_KEY
&& (operations.contains(ModelOperation.LISTEN) || operations.contains(ModelOperation.READ));
}

private boolean isReadRestrictingOwner(AuthRule authRule) {
Expand All @@ -170,15 +206,15 @@ private String getIdentityValue(String identityClaim, AuthorizationType authType
.getString(identityClaim);
} catch (JSONException error) {
throw new ApiAuthException(
"Attempted to subscribe to a model with owner-based authorization without " + identityClaim + " " +
"which was specified (or defaulted to) as the identity claim.",
"If you did not specify a custom identityClaim in your schema, make sure you are logged in. If " +
"you did, check that the value you specified in your schema is present in the access key."
"Attempted to subscribe to a model with owner-based authorization without " + identityClaim + " " +
"which was specified (or defaulted to) as the identity claim.",
"If you did not specify a custom identityClaim in your schema, make sure you are logged in. If " +
"you did, check that the value you specified in your schema is present in the access key."
);
} catch (CognitoParameterInvalidException error) {
throw new ApiAuthException(
"Failed to parse the ID token for identity claim: " + error.getMessage(),
"Please verify the validity of token vended by the registered auth provider."
"Failed to parse the ID token for identity claim: " + error.getMessage(),
"Please verify the validity of token vended by the registered auth provider."
);
}
}
Expand All @@ -197,13 +233,13 @@ private ArrayList<String> getUserGroups(String groupClaim, AuthorizationType aut
} catch (JSONException error) {
// This should not happen normally
throw new ApiException(
"Failed obtain group claim from the parsed JWT token.", error,
AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION
"Failed obtain group claim from the parsed JWT token.", error,
AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION
);
} catch (CognitoParameterInvalidException error) {
throw new ApiException(
"Failed to parse the ID token for group claim: " + error.getMessage(),
"Please verify the validity of token vended by the registered auth provider."
"Failed to parse the ID token for group claim: " + error.getMessage(),
"Please verify the validity of token vended by the registered auth provider."
);
}

Expand Down Expand Up @@ -245,9 +281,9 @@ private String getAuthToken(AuthorizationType authType) throws ApiException {
OidcAuthProvider oidcProvider = authProvider.getOidcAuthProvider();
if (oidcProvider == null) {
throw new ApiAuthException(
"OidcAuthProvider interface is not implemented.",
"Configure AWSApiPlugin with ApiAuthProviders containing an implementation of " +
"OidcAuthProvider interface that can vend a valid JWT token."
"OidcAuthProvider interface is not implemented.",
"Configure AWSApiPlugin with ApiAuthProviders containing an implementation of " +
"OidcAuthProvider interface that can vend a valid JWT token."
);
}
return oidcProvider.getLatestAuthToken();
Expand All @@ -256,10 +292,10 @@ private String getAuthToken(AuthorizationType authType) throws ApiException {
case NONE:
default:
throw new ApiAuthException(
"Tried to use owner/group-based authorization on an API that is not configured " +
"with either Cognito User Pools or OpenID Connect.",
"Verify that the API is configured with either Cognito User Pools or OpenID Connect. @auth " +
"with owner/group-based authorization is not supported for other modes."
"Tried to use owner/group-based authorization on an API that is not configured " +
"with either Cognito User Pools or OpenID Connect.",
"Verify that the API is configured with either Cognito User Pools or OpenID Connect. @auth " +
"with owner/group-based authorization is not supported for other modes."
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import org.junit.Test
class RealAWSCognitoAuthPluginTest {

private val dummyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2Mj" +
"M5MDIyfQ.e4RpZTfAb3oXkfq3IwHtR_8Zhn0U1JDV7McZPlBXyhw"
"M5MDIyfQ.e4RpZTfAb3oXkfq3IwHtR_8Zhn0U1JDV7McZPlBXyhw"

private var logger = mockk<Logger>(relaxed = true)
private val appClientId = "app Client Id"
Expand Down
1 change: 1 addition & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,7 @@ public final class com/amplifyframework/core/model/AuthRule {
public fun getOperationsOrDefault ()Ljava/util/List;
public fun getOwnerFieldOrDefault ()Ljava/lang/String;
public fun hashCode ()I
public fun ownerField (Lcom/amplifyframework/core/model/ModelSchema;)Lcom/amplifyframework/core/model/ModelField;
public fun toString ()Ljava/lang/String;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,19 @@ public String toString() {
'}';
}

/**
* Returns the owner if it's not set.
* @param schema specifies a schema from a request.
* @return owner field.
*/
public ModelField ownerField(ModelSchema schema) {
if (ownerField == null) {
return null;

}
return schema.getFields().get(this.ownerField);
}

/**
* Builder class for {@link AuthRule}.
*/
Expand Down
5 changes: 5 additions & 0 deletions jitpack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
jdk:
- openjdk17
install:
- echo "Running a custom install command"
- ./gradlew build publishToMavenLocal
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.amplifyframework.testutils.random.RandomString;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
Expand Down Expand Up @@ -273,6 +274,7 @@ public void downloadFileReturnsResult() throws InterruptedException {
* @throws InterruptedException not expected.
*/
@Test
@Ignore("Flaky")
public void downloadFileStoragePathReturnsResult() throws InterruptedException {
StorageDownloadFileResult result = StorageDownloadFileResult.fromFile(mock(File.class));
doAnswer(invocation -> {
Expand Down