diff --git a/api/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java b/api/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java index 5e6aab4eb94..348c7b825de 100644 --- a/api/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java +++ b/api/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java @@ -20,6 +20,7 @@ import org.projectnessie.client.api.ns.ClientSideGetMultipleNamespaces; import org.projectnessie.client.api.ns.ClientSideGetNamespace; import org.projectnessie.client.api.ns.ClientSideUpdateNamespace; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.Reference; /** @@ -32,6 +33,8 @@ */ public interface NessieApiV2 extends NessieApiV1 { + NessieUserInfo getUserInfo(); + GetRepositoryConfigBuilder getRepositoryConfig(); UpdateRepositoryConfigBuilder updateRepositoryConfig(); diff --git a/api/client/src/main/java/org/projectnessie/client/rest/v2/HttpApiV2.java b/api/client/src/main/java/org/projectnessie/client/rest/v2/HttpApiV2.java index 50780639e8a..a6cd034d3c5 100644 --- a/api/client/src/main/java/org/projectnessie/client/rest/v2/HttpApiV2.java +++ b/api/client/src/main/java/org/projectnessie/client/rest/v2/HttpApiV2.java @@ -40,6 +40,7 @@ import org.projectnessie.error.NessieNotFoundException; import org.projectnessie.model.Branch; import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.Reference; import org.projectnessie.model.SingleReferenceResponse; @@ -67,6 +68,11 @@ public NessieConfiguration getConfig() { return client.newRequest().path("config").get().readEntity(NessieConfiguration.class); } + @Override + public NessieUserInfo getUserInfo() { + return client.newRequest().path("config/whoami").get().readEntity(NessieUserInfo.class); + } + @Override public Branch getDefaultBranch() throws NessieNotFoundException { return (Branch) diff --git a/api/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java b/api/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java index 0028508de46..b277ac70c57 100644 --- a/api/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java +++ b/api/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java @@ -18,6 +18,7 @@ import java.util.List; import org.projectnessie.error.NessieConflictException; import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.RepositoryConfigResponse; import org.projectnessie.model.UpdateRepositoryConfigRequest; import org.projectnessie.model.UpdateRepositoryConfigResponse; @@ -35,4 +36,6 @@ public interface ConfigApi { UpdateRepositoryConfigResponse updateRepositoryConfig( UpdateRepositoryConfigRequest repositoryConfigUpdate) throws NessieConflictException; + + NessieUserInfo getUserInfo(); } diff --git a/api/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java b/api/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java index 2bafb024573..49b98ff0988 100644 --- a/api/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java +++ b/api/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java @@ -33,6 +33,7 @@ import org.projectnessie.api.v2.ConfigApi; import org.projectnessie.error.NessieConflictException; import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.RepositoryConfigResponse; import org.projectnessie.model.UpdateRepositoryConfigRequest; import org.projectnessie.model.UpdateRepositoryConfigResponse; @@ -123,4 +124,26 @@ RepositoryConfigResponse getRepositoryConfig( @JsonView(Views.V2.class) UpdateRepositoryConfigResponse updateRepositoryConfig( UpdateRepositoryConfigRequest repositoryConfigUpdate) throws NessieConflictException; + + @Override + @GET + @jakarta.ws.rs.GET + @Produces(MediaType.APPLICATION_JSON) + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @Operation(summary = "Returns information about the current user.", operationId = "getUserInfo") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "User information", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = NessieUserInfo.class), + examples = {@ExampleObject(ref = "nessieUserInfo")})), + @APIResponse(responseCode = "401", description = "Invalid credentials provided") + }) + @JsonView(Views.V2.class) + @Path("whoami") + @jakarta.ws.rs.Path("whoami") + NessieUserInfo getUserInfo(); } diff --git a/api/model/src/main/java/org/projectnessie/model/NessieUserInfo.java b/api/model/src/main/java/org/projectnessie/model/NessieUserInfo.java new file mode 100644 index 00000000000..f4b881b001f --- /dev/null +++ b/api/model/src/main/java/org/projectnessie/model/NessieUserInfo.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed 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.projectnessie.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; +import javax.annotation.Nullable; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.immutables.value.Value; + +@Schema( + type = SchemaType.OBJECT, + title = "NessieUserInfo", + description = "Information about the current user.") +@Value.Immutable +@JsonSerialize(as = ImmutableNessieUserInfo.class) +@JsonDeserialize(as = ImmutableNessieUserInfo.class) +public interface NessieUserInfo { + @Value.Parameter(order = 1) + boolean anonymous(); + + @Value.Parameter(order = 2) + @Nullable + @jakarta.annotation.Nullable + String name(); + + @Value.Parameter(order = 3) + List roles(); + + static NessieUserInfo nessieUserInfo(boolean anonymous, String name, List roles) { + return null; // ImmutableNessieUserInfo.of(anonymous, name, roles); + } + + static Builder builder() { + return ImmutableNessieUserInfo.builder(); + } + + interface Builder { + Builder anonymous(boolean anonymous); + + Builder name(@Nullable String name); + + Builder roles(Iterable roles); + + Builder addRoles(String role); + + Builder addRoles(String... role); + + Builder addAllRoles(Iterable role); + + NessieUserInfo build(); + } +} diff --git a/api/model/src/main/resources/META-INF/openapi.yaml b/api/model/src/main/resources/META-INF/openapi.yaml index 3a5af06bae6..3efa2d804bc 100644 --- a/api/model/src/main/resources/META-INF/openapi.yaml +++ b/api/model/src/main/resources/META-INF/openapi.yaml @@ -77,6 +77,15 @@ components: maxSupportedApiVersion: 2 specVersion: "2.0.0" + nessieUserInfo: + value: + anonymous: false + name: username + roles: + - role_one + - role_two + - role_three + namespace: value: "a.b.c" diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/BaseTestNessieRest.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/BaseTestNessieRest.java index 146f34c9218..904574be968 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/BaseTestNessieRest.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/BaseTestNessieRest.java @@ -16,6 +16,7 @@ package org.projectnessie.jaxrs.tests; import static io.restassured.RestAssured.given; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.InstanceOfAssertFactories.list; @@ -70,6 +71,7 @@ import org.projectnessie.model.ImmutableBranch; import org.projectnessie.model.ImmutableOperations; import org.projectnessie.model.Namespace; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.Operation.Put; import org.projectnessie.model.Reference; import org.projectnessie.model.SingleReferenceResponse; @@ -166,6 +168,15 @@ public void testNotFoundUrls(String path) { rest().head(path).then().statusCode(404); } + @Test + @NessieApiVersions(versions = {NessieApiVersion.V2}) + public void testUserInfo() { + NessieUserInfo userInfo = ((NessieApiV2) this.api()).getUserInfo(); + soft.assertThat(userInfo) + .extracting(NessieUserInfo::anonymous, NessieUserInfo::name, NessieUserInfo::roles) + .containsExactly(true, null, emptyList()); + } + @Test @NessieApiVersions(versions = {NessieApiVersion.V1}) public void testReferenceConflictDetailsV1() { diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/TestBasicAuthentication.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/TestBasicAuthentication.java index 0202efb4668..fca99685f9d 100644 --- a/servers/quarkus-server/src/test/java/org/projectnessie/server/TestBasicAuthentication.java +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/TestBasicAuthentication.java @@ -15,6 +15,7 @@ */ package org.projectnessie.server; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -23,8 +24,12 @@ import io.quarkus.test.junit.TestProfile; import java.util.Map; import org.junit.jupiter.api.Test; +import org.projectnessie.client.api.NessieApiV2; import org.projectnessie.client.auth.BasicAuthenticationProvider; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.client.rest.NessieNotAuthorizedException; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.server.authn.AuthenticationEnabledProfile; @SuppressWarnings("resource") // api() returns an AutoCloseable @@ -39,6 +44,17 @@ void testValidCredentials() throws Exception { assertThat(api().getAllReferences().stream()).isNotEmpty(); } + @Test + @NessieApiVersions(versions = {NessieApiVersion.V2}) + public void testUserInfo() { + withClientCustomizer( + c -> c.withAuthentication(BasicAuthenticationProvider.create("test_user", "test_user"))); + NessieUserInfo userInfo = ((NessieApiV2) this.api()).getUserInfo(); + assertThat(userInfo) + .extracting(NessieUserInfo::anonymous, NessieUserInfo::name, NessieUserInfo::roles) + .containsExactly(false, "test_user", singletonList("test123")); + } + @Test void testValidAdminCredentials() throws Exception { withClientCustomizer( diff --git a/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java index 043cf8075b0..570afe9680e 100644 --- a/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java +++ b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java @@ -19,6 +19,7 @@ import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.Path; +import java.security.Principal; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -27,6 +28,7 @@ import org.projectnessie.model.ImmutableRepositoryConfigResponse; import org.projectnessie.model.ImmutableUpdateRepositoryConfigResponse; import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.RepositoryConfig; import org.projectnessie.model.RepositoryConfigResponse; import org.projectnessie.model.UpdateRepositoryConfigRequest; @@ -45,6 +47,7 @@ public class RestV2ConfigResource implements HttpConfigApi { private final ConfigApiImpl config; + private final AccessContext accessContext; // Mandated by CDI 2.0 public RestV2ConfigResource() { @@ -54,6 +57,7 @@ public RestV2ConfigResource() { @Inject public RestV2ConfigResource( ServerConfig config, VersionStore store, Authorizer authorizer, AccessContext accessContext) { + this.accessContext = accessContext; this.config = new ConfigApiImpl(config, store, authorizer, accessContext, 2); } @@ -81,4 +85,24 @@ public UpdateRepositoryConfigResponse updateRepositoryConfig( .previous(config.updateRepositoryConfig(repositoryConfigUpdate.getConfig())) .build(); } + + @Override + public NessieUserInfo getUserInfo() { + NessieUserInfo.Builder userInfo = NessieUserInfo.builder(); + + Principal user = accessContext.user(); + if (user != null) { + String name = user.getName(); + if (name != null && !name.isEmpty()) { + userInfo.name(user.getName()); + accessContext.roleIds().forEach(userInfo::addRoles); + userInfo.anonymous(false); + } else { + userInfo.anonymous(true); + } + } else { + userInfo.anonymous(true); + } + return userInfo.build(); + } } diff --git a/testing/combined-cs/src/main/java/org/projectnessie/nessie/combined/CombinedClientImpl.java b/testing/combined-cs/src/main/java/org/projectnessie/nessie/combined/CombinedClientImpl.java index 9aaa0eae8b1..bdd7d3dfb9d 100644 --- a/testing/combined-cs/src/main/java/org/projectnessie/nessie/combined/CombinedClientImpl.java +++ b/testing/combined-cs/src/main/java/org/projectnessie/nessie/combined/CombinedClientImpl.java @@ -44,6 +44,7 @@ import org.projectnessie.error.NessieNotFoundException; import org.projectnessie.model.Branch; import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.NessieUserInfo; import org.projectnessie.model.Reference; final class CombinedClientImpl implements NessieApiV2 { @@ -151,6 +152,11 @@ public UpdateRepositoryConfigBuilder updateRepositoryConfig() { return new CombinedUpdateRepositoryConfig(configApi); } + @Override + public NessieUserInfo getUserInfo() { + return configApi.getUserInfo(); + } + @Override public DeleteReferenceBuilder deleteReference() { return new CombinedDeleteReference(treeApi);