Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.exceptions;

import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;

/**
* Bad-request exception raised when an OAuth2 token-endpoint response carries one of the {@code
* 400}-class error codes from RFC 6749 §5.2 (e.g. {@code invalid_request}, {@code invalid_grant},
* {@code unauthorized_client}, {@code unsupported_grant_type}, {@code invalid_scope}).
*/
public class OAuth2BadRequestException extends BadRequestException implements OAuth2Error {
private final String errorType;

@FormatMethod
public OAuth2BadRequestException(String errorType, @FormatString String message, Object... args) {
super(message, args);
this.errorType = errorType;
}

@FormatMethod
public OAuth2BadRequestException(
String errorType, Throwable cause, @FormatString String message, Object... args) {
super(cause, message, args);
this.errorType = errorType;
}

@Override
public String errorType() {
return errorType;
}
}
29 changes: 29 additions & 0 deletions api/src/main/java/org/apache/iceberg/exceptions/OAuth2Error.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.exceptions;

/**
* Marker interface for exceptions arising from an OAuth2 token-endpoint error response (RFC 6749
* §5.2). The {@link #errorType()} value is one of the RFC 6749 §5.2 error codes (e.g. {@code
* "invalid_grant"}, {@code "invalid_client"}).
*/
public interface OAuth2Error {
/** The OAuth2 error code from RFC 6749 §5.2 (e.g. {@code "invalid_grant"}). */
String errorType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.exceptions;

import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;

/**
* Not-authorized exception raised when an OAuth2 token-endpoint response carries the {@code
* invalid_client} error code from RFC 6749 §5.2.
*/
public class OAuth2NotAuthorizedException extends NotAuthorizedException implements OAuth2Error {
private final String errorType;

@FormatMethod
public OAuth2NotAuthorizedException(
String errorType, @FormatString String message, Object... args) {
super(message, args);
this.errorType = errorType;
}

@FormatMethod
public OAuth2NotAuthorizedException(
String errorType, Throwable cause, @FormatString String message, Object... args) {
super(cause, message, args);
this.errorType = errorType;
}

@Override
public String errorType() {
return errorType;
}
}
10 changes: 6 additions & 4 deletions core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.apache.iceberg.exceptions.NoSuchWarehouseException;
import org.apache.iceberg.exceptions.NotAuthorizedException;
import org.apache.iceberg.exceptions.NotFoundException;
import org.apache.iceberg.exceptions.OAuth2BadRequestException;
import org.apache.iceberg.exceptions.OAuth2NotAuthorizedException;
import org.apache.iceberg.exceptions.RESTException;
import org.apache.iceberg.exceptions.ServiceFailureException;
import org.apache.iceberg.exceptions.ServiceUnavailableException;
Expand Down Expand Up @@ -376,15 +378,15 @@ public void accept(ErrorResponse error) {
if (error.type() != null) {
switch (error.type()) {
case OAuth2Properties.INVALID_CLIENT_ERROR:
throw new NotAuthorizedException(
"Not authorized: %s: %s", error.type(), error.message());
throw new OAuth2NotAuthorizedException(
error.type(), "Not authorized: %s: %s", error.type(), error.message());
case OAuth2Properties.INVALID_REQUEST_ERROR:
case OAuth2Properties.INVALID_GRANT_ERROR:
case OAuth2Properties.UNAUTHORIZED_CLIENT_ERROR:
case OAuth2Properties.UNSUPPORTED_GRANT_TYPE_ERROR:
case OAuth2Properties.INVALID_SCOPE_ERROR:
throw new BadRequestException(
"Malformed request: %s: %s", error.type(), error.message());
throw new OAuth2BadRequestException(
error.type(), "Malformed request: %s: %s", error.type(), error.message());
}
}
throw createRESTException(error);
Expand Down
106 changes: 106 additions & 0 deletions core/src/test/java/org/apache/iceberg/rest/TestErrorHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.apache.iceberg.exceptions.NoSuchWarehouseException;
import org.apache.iceberg.exceptions.OAuth2BadRequestException;
import org.apache.iceberg.exceptions.OAuth2NotAuthorizedException;
import org.apache.iceberg.exceptions.RESTException;
import org.apache.iceberg.exceptions.ServiceFailureException;
import org.apache.iceberg.rest.auth.OAuth2Properties;
import org.apache.iceberg.rest.responses.ErrorResponse;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;

public class TestErrorHandlers {
Expand Down Expand Up @@ -104,4 +108,106 @@ public void testConfigErrorHandlerDelegatesToDefaultForNon404() {
.isInstanceOf(ServiceFailureException.class)
.hasMessageContaining("Internal server error");
}

@Test
public void testOAuthErrorHandlerInvalidGrant() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(400)
.withType(OAuth2Properties.INVALID_GRANT_ERROR)
.withMessage("token expired")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2BadRequestException.class)
.hasMessage("Malformed request: invalid_grant: token expired")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2BadRequestException.class))
.extracting(OAuth2BadRequestException::errorType)
.isEqualTo(OAuth2Properties.INVALID_GRANT_ERROR);
}

@Test
public void testOAuthErrorHandlerInvalidClient() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(401)
.withType(OAuth2Properties.INVALID_CLIENT_ERROR)
.withMessage("bad credentials")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2NotAuthorizedException.class)
.hasMessage("Not authorized: invalid_client: bad credentials")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2NotAuthorizedException.class))
.extracting(OAuth2NotAuthorizedException::errorType)
.isEqualTo(OAuth2Properties.INVALID_CLIENT_ERROR);
}

@Test
public void testOAuthErrorHandlerInvalidRequest() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(400)
.withType(OAuth2Properties.INVALID_REQUEST_ERROR)
.withMessage("missing parameter")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2BadRequestException.class)
.hasMessage("Malformed request: invalid_request: missing parameter")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2BadRequestException.class))
.extracting(OAuth2BadRequestException::errorType)
.isEqualTo(OAuth2Properties.INVALID_REQUEST_ERROR);
}

@Test
public void testOAuthErrorHandlerUnauthorizedClient() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(400)
.withType(OAuth2Properties.UNAUTHORIZED_CLIENT_ERROR)
.withMessage("client cannot use this grant")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2BadRequestException.class)
.hasMessage("Malformed request: unauthorized_client: client cannot use this grant")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2BadRequestException.class))
.extracting(OAuth2BadRequestException::errorType)
.isEqualTo(OAuth2Properties.UNAUTHORIZED_CLIENT_ERROR);
}

@Test
public void testOAuthErrorHandlerUnsupportedGrantType() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(400)
.withType(OAuth2Properties.UNSUPPORTED_GRANT_TYPE_ERROR)
.withMessage("unsupported grant")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2BadRequestException.class)
.hasMessage("Malformed request: unsupported_grant_type: unsupported grant")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2BadRequestException.class))
.extracting(OAuth2BadRequestException::errorType)
.isEqualTo(OAuth2Properties.UNSUPPORTED_GRANT_TYPE_ERROR);
}

@Test
public void testOAuthErrorHandlerInvalidScope() {
ErrorResponse error =
ErrorResponse.builder()
.responseCode(400)
.withType(OAuth2Properties.INVALID_SCOPE_ERROR)
.withMessage("scope not allowed")
.build();

assertThatThrownBy(() -> ErrorHandlers.oauthErrorHandler().accept(error))
.isInstanceOf(OAuth2BadRequestException.class)
.hasMessage("Malformed request: invalid_scope: scope not allowed")
.asInstanceOf(InstanceOfAssertFactories.type(OAuth2BadRequestException.class))
.extracting(OAuth2BadRequestException::errorType)
.isEqualTo(OAuth2Properties.INVALID_SCOPE_ERROR);
}
}
Loading