From 11b7e718042d490fca53d923035c6f93707e740b Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Thu, 16 May 2024 15:11:23 +0100 Subject: [PATCH] Enable Basic Auth for testing purposes (#4632) * WIP Basic Auth in Registry * more and test in ci * remove env * fix docs for now * tests * more * fixes * comment failing test * fix the test * wip UI integration of basic auth * ui implementation of basic auth * last working setup * cleanup * fixes * review * bump common-app-components * finalize * update deps * more * fix pom --------- Co-authored-by: Andrea Peruffo --- .github/workflows/verify.yaml | 4 +- .gitignore | 2 +- .../io/apicurio/registry/auth/AuthConfig.java | 23 +- .../registry/auth/AuthorizedInterceptor.java | 2 +- .../registry/rest/v3/SystemResourceImpl.java | 5 +- app/src/main/resources/application.properties | 3 + .../auth/BasicAuthWithPropertiesTest.java | 364 ++++++++++++++++++ .../src/main/resources/META-INF/openapi.json | 1 + .../ref-registry-all-configs.adoc | 5 + examples/developer-basic-auth/basic-auth.env | 10 + .../basic-auth.properties | 21 + pom.xml | 2 +- ui/ui-app/package-lock.json | 70 +++- ui/ui-app/package.json | 3 +- ui/ui-app/src/app/components/auth/IfAuth.tsx | 2 +- ui/ui-app/src/services/useAdminService.ts | 65 ++-- ui/ui-app/src/services/useGroupsService.ts | 98 ++--- ui/ui-app/src/services/useUserService.ts | 9 +- ui/ui-app/src/utils/rest.utils.ts | 20 + ui/ui-app/vite.config.ts | 2 +- .../BasicAuthWithPropertiesTestProfile.java | 37 ++ 21 files changed, 641 insertions(+), 107 deletions(-) create mode 100644 app/src/test/java/io/apicurio/registry/auth/BasicAuthWithPropertiesTest.java create mode 100644 examples/developer-basic-auth/basic-auth.env create mode 100644 examples/developer-basic-auth/basic-auth.properties create mode 100644 utils/tests/src/main/java/io/apicurio/registry/utils/tests/BasicAuthWithPropertiesTestProfile.java diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index fb7309af53..4681603752 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -306,7 +306,7 @@ jobs: build-verify-python-sdk: name: Verify Python SDK runs-on: ubuntu-latest - # if: github.repository_owner == 'Apicurio' && !contains(github.event.*.labels.*.name, 'DO NOT MERGE') + if: github.repository_owner == 'Apicurio' && !contains(github.event.*.labels.*.name, 'DO NOT MERGE') steps: - name: Checkout Code with Ref '${{ github.ref }}' uses: actions/checkout@v3 @@ -343,7 +343,7 @@ jobs: build-verify-go-sdk: name: Verify Go SDK runs-on: ubuntu-latest - # if: github.repository_owner == 'Apicurio' && !contains(github.event.*.labels.*.name, 'DO NOT MERGE') + if: github.repository_owner == 'Apicurio' && !contains(github.event.*.labels.*.name, 'DO NOT MERGE') steps: - name: Checkout Code with Ref '${{ github.ref }}' uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 99a42ba1b3..1cc662ad1d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ python-sdk/apicurioregistrysdk/client python-sdk/openapi.json __pycache__ - +.env diff --git a/app/src/main/java/io/apicurio/registry/auth/AuthConfig.java b/app/src/main/java/io/apicurio/registry/auth/AuthConfig.java index 9ac6393849..100f3186d9 100644 --- a/app/src/main/java/io/apicurio/registry/auth/AuthConfig.java +++ b/app/src/main/java/io/apicurio/registry/auth/AuthConfig.java @@ -18,7 +18,17 @@ public class AuthConfig { Logger log; @ConfigProperty(name = "quarkus.oidc.tenant-enabled", defaultValue = "false") - boolean authenticationEnabled; + @Info(category = "auth", description = "Enable auth", availableSince = "0.1.18-SNAPSHOT", registryAvailableSince = "2.0.0.Final", studioAvailableSince = "1.0.0") + boolean oidcAuthEnabled; + + @Dynamic(label = "HTTP basic authentication", description = "When selected, users are permitted to authenticate using HTTP basic authentication (in addition to OAuth).", requires = "apicurio.authn.enabled=true") + @ConfigProperty(name = "apicurio.authn.basic-client-credentials.enabled", defaultValue = "false") + @Info(category = "auth", description = "Enable basic auth client credentials", availableSince = "0.1.18-SNAPSHOT", registryAvailableSince = "2.1.0.Final", studioAvailableSince = "1.0.0") + Supplier basicClientCredentialsAuthEnabled; + + @ConfigProperty(name = "quarkus.http.auth.basic", defaultValue = "false") + @Info(category = "auth", description = "Enable basic auth", availableSince = "1.1.X-SNAPSHOT", registryAvailableSince = "3.X.X.Final", studioAvailableSince = "1.0.0") + boolean basicAuthEnabled; @ConfigProperty(name = "apicurio.auth.role-based-authorization", defaultValue = "false") @Info(category = "auth", description = "Enable role based authorization", availableSince = "2.1.0.Final") @@ -97,7 +107,8 @@ public class AuthConfig { @PostConstruct void onConstruct() { log.debug("==============================="); - log.debug("Auth Enabled: " + authenticationEnabled); + log.debug("OIDC Auth Enabled: " + oidcAuthEnabled); + log.debug("Basic Auth Enabled: " + basicAuthEnabled); log.debug("Anonymous Read Access Enabled: " + anonymousReadAccessEnabled); log.debug("Authenticated Read Access Enabled: " + authenticatedReadAccessEnabled); log.debug("RBAC Enabled: " + roleBasedAuthorizationEnabled); @@ -117,8 +128,12 @@ void onConstruct() { log.debug("==============================="); } - public boolean isAuthEnabled() { - return this.authenticationEnabled; + public boolean isOidcAuthEnabled() { + return this.oidcAuthEnabled; + } + + public boolean isBasicAuthEnabled() { + return this.basicAuthEnabled; } public boolean isRbacEnabled() { diff --git a/app/src/main/java/io/apicurio/registry/auth/AuthorizedInterceptor.java b/app/src/main/java/io/apicurio/registry/auth/AuthorizedInterceptor.java index 1e8ab73e1f..bbd8e33a5f 100644 --- a/app/src/main/java/io/apicurio/registry/auth/AuthorizedInterceptor.java +++ b/app/src/main/java/io/apicurio/registry/auth/AuthorizedInterceptor.java @@ -60,7 +60,7 @@ public Object authorizeMethod(InvocationContext context) throws Exception { } // If authentication is not enabled, just do it. - if (!authConfig.authenticationEnabled) { + if (!authConfig.oidcAuthEnabled && !authConfig.basicAuthEnabled) { return context.proceed(); } diff --git a/app/src/main/java/io/apicurio/registry/rest/v3/SystemResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v3/SystemResourceImpl.java index 0e3c19b0ef..48d5db5f1f 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v3/SystemResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v3/SystemResourceImpl.java @@ -103,8 +103,9 @@ private UserInterfaceConfigAuth uiAuthConfig() { UserInterfaceConfigAuth rval = new UserInterfaceConfigAuth(); rval.setObacEnabled(authConfig.isObacEnabled()); rval.setRbacEnabled(authConfig.isRbacEnabled()); - rval.setType(authConfig.isAuthEnabled() ? UserInterfaceConfigAuth.Type.oidc : UserInterfaceConfigAuth.Type.none); - if (authConfig.isAuthEnabled()) { + rval.setType(authConfig.isOidcAuthEnabled() ? UserInterfaceConfigAuth.Type.oidc : + authConfig.isBasicAuthEnabled() ? UserInterfaceConfigAuth.Type.basic : UserInterfaceConfigAuth.Type.none); + if (authConfig.isOidcAuthEnabled()) { Map options = new HashMap<>(); options.put("url", uiConfig.authOidcUrl); options.put("redirectUri", uiConfig.authOidcRedirectUri); diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index f3d6949177..9cea9395b1 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -8,6 +8,9 @@ quarkus.oidc.token-path=https://auth.apicur.io/auth/realms/apicurio-local/protoc quarkus.oidc.client-id=registry-api quarkus.http.auth.proactive=false +# Build time property to enable username and password SecurityIdentity +quarkus.security.users.embedded.enabled=true + # HTTP quarkus.http.port=8080 quarkus.http.non-application-root-path=/ diff --git a/app/src/test/java/io/apicurio/registry/auth/BasicAuthWithPropertiesTest.java b/app/src/test/java/io/apicurio/registry/auth/BasicAuthWithPropertiesTest.java new file mode 100644 index 0000000000..f83a36a00e --- /dev/null +++ b/app/src/test/java/io/apicurio/registry/auth/BasicAuthWithPropertiesTest.java @@ -0,0 +1,364 @@ +package io.apicurio.registry.auth; + +import com.microsoft.kiota.ApiException; +import io.apicurio.registry.AbstractResourceTestBase; +import io.apicurio.registry.rest.client.RegistryClient; +import io.apicurio.registry.rest.client.models.*; +import io.apicurio.registry.rules.compatibility.CompatibilityLevel; +import io.apicurio.registry.rules.validity.ValidityLevel; +import io.apicurio.registry.types.ArtifactType; +import io.apicurio.registry.utils.tests.*; +import io.kiota.http.vertx.VertXRequestAdapter; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.Base64; +import java.util.UUID; + +import static io.apicurio.registry.client.auth.VertXAuthFactory.buildSimpleAuthWebClient; +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +@TestProfile(BasicAuthWithPropertiesTestProfile.class) +@Tag(ApicurioTestTags.SLOW) +public class BasicAuthWithPropertiesTest extends AbstractResourceTestBase { + + private static final String ARTIFACT_CONTENT = "{\"name\":\"redhat\"}"; + + final String groupId = "authTestGroupId"; + + public static final String ADMIN_USERNAME = "alice"; + public static final String ADMIN_PASSWORD = "alice"; + public static final String DEVELOPER_USERNAME = "bob1"; + public static final String DEVELOPER_PASSWORD = "bob1"; + public static final String DEVELOPER_2_USERNAME = "bob2"; + public static final String DEVELOPER_2_PASSWORD = "bob2"; + public static final String READONLY_USERNAME = "duncan"; + public static final String READONLY_PASSWORD = "duncan"; + + + @Override + protected RegistryClient createRestClientV3() { + var adapter =new VertXRequestAdapter(buildSimpleAuthWebClient(ADMIN_USERNAME, ADMIN_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + return new RegistryClient(adapter); + } + + private static final ArtifactContent content = new ArtifactContent(); + static { + content.setContent("{}"); + } + + protected void assertArtifactNotFound(Exception exception) { + Assertions.assertEquals(io.apicurio.registry.rest.client.models.Error.class, exception.getClass()); + Assertions.assertEquals("ArtifactNotFoundException", ((io.apicurio.registry.rest.client.models.Error)exception).getName()); + Assertions.assertEquals(404, ((io.apicurio.registry.rest.client.models.Error)exception).getErrorCode()); + } + + @Test + public void testWrongCreds() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + var exception = Assertions.assertThrows(ApiException.class, () -> { + client.groups().byGroupId(groupId).artifacts().get(); + }); + assertEquals(401, exception.getResponseStatusCode()); + } + + @Test + public void testReadOnly() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(READONLY_USERNAME, READONLY_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + String artifactId = TestUtils.generateArtifactId(); + client.groups().byGroupId(groupId).artifacts().get(); + var exception1 = Assertions.assertThrows(Exception.class, () -> { + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + }); + assertArtifactNotFound(exception1); + var exception2 = Assertions.assertThrows(Exception.class, () -> { + client.groups().byGroupId("abc").artifacts().byArtifactId(artifactId).get(); + }); + assertArtifactNotFound(exception2); + var exception3 = Assertions.assertThrows(Exception.class, () -> { + client.groups().byGroupId("testReadOnly").artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + }); + assertForbidden(exception3); + + var devAdapter = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + devAdapter.setBaseUrl(registryV3ApiUrl); + RegistryClient devClient = new RegistryClient(devAdapter); + + VersionMetaData meta = devClient.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + + TestUtils.retry(() -> devClient.groups().byGroupId(groupId).artifacts().byArtifactId(meta.getArtifactId()).get()); + + assertNotNull(client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get()); + + UserInfo userInfo = client.users().me().get(); + assertNotNull(userInfo); + Assertions.assertEquals(READONLY_USERNAME, userInfo.getUsername()); + Assertions.assertFalse(userInfo.getAdmin()); + Assertions.assertFalse(userInfo.getDeveloper()); + Assertions.assertTrue(userInfo.getViewer()); + } + + @Test + public void testDevRole() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + String artifactId = TestUtils.generateArtifactId(); + try { + client.groups().byGroupId(groupId).artifacts().get(); + + client.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + TestUtils.retry(() -> client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get()); + + assertTrue(client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("branch=latest").content().get().readAllBytes().length > 0); + + Rule ruleConfig = new Rule(); + ruleConfig.setType(RuleType.VALIDITY); + ruleConfig.setConfig(ValidityLevel.NONE.name()); + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).rules().post(ruleConfig); + + var exception = Assertions.assertThrows(Exception.class, () -> { + client.admin().rules().post(ruleConfig); + }); + assertForbidden(exception); + + UserInfo userInfo = client.users().me().get(); + assertNotNull(userInfo); + Assertions.assertEquals(DEVELOPER_USERNAME, userInfo.getUsername()); + Assertions.assertFalse(userInfo.getAdmin()); + Assertions.assertTrue(userInfo.getDeveloper()); + Assertions.assertFalse(userInfo.getViewer()); + } finally { + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).delete(); + } + } + + @Test + public void testAdminRole() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(ADMIN_USERNAME, ADMIN_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + String artifactId = TestUtils.generateArtifactId(); + try { + client.groups().byGroupId(groupId).artifacts().get(); + + client.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + TestUtils.retry(() -> client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get()); + + assertTrue(client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("branch=latest").content().get().readAllBytes().length > 0); + + Rule ruleConfig = new Rule(); + ruleConfig.setType(RuleType.VALIDITY); + ruleConfig.setConfig(ValidityLevel.NONE.name()); + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).rules().post(ruleConfig); + + client.admin().rules().post(ruleConfig); + + UserInfo userInfo = client.users().me().get(); + assertNotNull(userInfo); + Assertions.assertEquals(ADMIN_USERNAME, userInfo.getUsername()); + Assertions.assertTrue(userInfo.getAdmin()); + Assertions.assertFalse(userInfo.getDeveloper()); + Assertions.assertFalse(userInfo.getViewer()); + } finally { + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).delete(); + } + } + + @Test + public void testOwnerOnlyAuthorization() throws Exception { + var devAdapter = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + devAdapter.setBaseUrl(registryV3ApiUrl); + RegistryClient clientDev = new RegistryClient(devAdapter); + + var adminAdapter = new VertXRequestAdapter(buildSimpleAuthWebClient(ADMIN_USERNAME, ADMIN_PASSWORD)); + adminAdapter.setBaseUrl(registryV3ApiUrl); + RegistryClient clientAdmin = new RegistryClient(adminAdapter); + + // Admin user will create an artifact + String artifactId = TestUtils.generateArtifactId(); + clientAdmin.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + + EditableArtifactMetaData updatedMetaData = new EditableArtifactMetaData(); + updatedMetaData.setName("Updated Name"); + // Dev user cannot edit the same artifact because Dev user is not the owner + var exception1 = Assertions.assertThrows(Exception.class, () -> { + clientDev.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).put(updatedMetaData); + }); + assertForbidden(exception1); + + // But the admin user CAN make the change. + clientAdmin.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).put(updatedMetaData); + + + // Now the Dev user will create an artifact + String artifactId2 = TestUtils.generateArtifactId(); + clientDev.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId2); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + + // And the Admin user will modify it (allowed because it's the Admin user) + Rule rule = new Rule(); + rule.setType(RuleType.COMPATIBILITY); + rule.setConfig(CompatibilityLevel.BACKWARD.name()); + clientAdmin.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId2).rules().post(rule); + } + + @Test + public void testGetArtifactOwner() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + + //Preparation + final String groupId = "testGetArtifactOwner"; + final String artifactId = generateArtifactId(); + final String version = "1"; + + //Execution + var artifactContent = new ArtifactContent(); + artifactContent.setContent(ARTIFACT_CONTENT); + final VersionMetaData created = client.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.queryParameters.ifExists = IfExists.FAIL; + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + }); + + //Assertions + assertNotNull(created); + assertEquals(groupId, created.getGroupId()); + assertEquals(artifactId, created.getArtifactId()); + assertEquals(version, created.getVersion()); + assertEquals(DEVELOPER_USERNAME, created.getOwner()); + + //Get the artifact owner via the REST API and verify it + ArtifactMetaData amd = client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + assertEquals(DEVELOPER_USERNAME, amd.getOwner()); + } + + @Test + public void testUpdateArtifactOwner() throws Exception { + var adapter = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + adapter.setBaseUrl(registryV3ApiUrl); + RegistryClient client = new RegistryClient(adapter); + + //Preparation + final String groupId = "testUpdateArtifactOwner"; + final String artifactId = generateArtifactId(); + + final String version = "1.0"; + final String name = "testUpdateArtifactOwnerName"; + final String description = "testUpdateArtifactOwnerDescription"; + + //Execution + var artifactContent = new ArtifactContent(); + artifactContent.setContent(ARTIFACT_CONTENT); + final VersionMetaData created = client.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.queryParameters.ifExists = IfExists.FAIL; + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + config.headers.add("X-Registry-Version", version); + config.headers.add("X-Registry-Name-Encoded", Base64.getEncoder().encodeToString(name.getBytes())); + config.headers.add("X-Registry-Description-Encoded", Base64.getEncoder().encodeToString(description.getBytes())); + }); + + //Assertions + assertNotNull(created); + assertEquals(groupId, created.getGroupId()); + assertEquals(artifactId, created.getArtifactId()); + assertEquals(version, created.getVersion()); + assertEquals(DEVELOPER_USERNAME, created.getOwner()); + + //Get the artifact owner via the REST API and verify it + ArtifactMetaData amd = client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + assertEquals(DEVELOPER_USERNAME, amd.getOwner()); + + //Update the owner + EditableArtifactMetaData eamd = new EditableArtifactMetaData(); + eamd.setOwner(DEVELOPER_2_USERNAME); + client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).put(eamd); + + //Check that the update worked + amd = client.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + assertEquals(DEVELOPER_2_USERNAME, amd.getOwner()); + } + + @Test + public void testUpdateArtifactOwnerOnlyByOwner() throws Exception { + var adapter_dev1 = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_USERNAME, DEVELOPER_PASSWORD)); + adapter_dev1.setBaseUrl(registryV3ApiUrl); + RegistryClient client_dev1 = new RegistryClient(adapter_dev1); + var adapter_dev2 = new VertXRequestAdapter(buildSimpleAuthWebClient(DEVELOPER_2_USERNAME, DEVELOPER_2_PASSWORD)); + adapter_dev2.setBaseUrl(registryV3ApiUrl); + RegistryClient client_dev2 = new RegistryClient(adapter_dev2); + + //Preparation + final String groupId = "testUpdateArtifactOwnerOnlyByOwner"; + final String artifactId = generateArtifactId(); + + final String version = "1.0"; + final String name = "testUpdateArtifactOwnerOnlyByOwnerName"; + final String description = "testUpdateArtifactOwnerOnlyByOwnerDescription"; + + //Execution + var artifactContent = new ArtifactContent(); + artifactContent.setContent(ARTIFACT_CONTENT); + final VersionMetaData created = client_dev1.groups().byGroupId(groupId).artifacts().post(content, config -> { + config.queryParameters.ifExists = IfExists.FAIL; + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.JSON); + config.headers.add("X-Registry-Version", version); + config.headers.add("X-Registry-Name-Encoded", Base64.getEncoder().encodeToString(name.getBytes())); + config.headers.add("X-Registry-Description-Encoded", Base64.getEncoder().encodeToString(description.getBytes())); + }); + + //Assertions + assertNotNull(created); + assertEquals(groupId, created.getGroupId()); + assertEquals(artifactId, created.getArtifactId()); + assertEquals(version, created.getVersion()); + assertEquals(DEVELOPER_USERNAME, created.getOwner()); + + //Get the artifact owner via the REST API and verify it + ArtifactMetaData amd = client_dev1.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + assertEquals(DEVELOPER_USERNAME, amd.getOwner()); + + //Try to update the owner by dev2 (should fail) + var exception1 = assertThrows(Exception.class, () -> { + EditableArtifactMetaData eamd = new EditableArtifactMetaData(); + eamd.setOwner(DEVELOPER_2_USERNAME); + client_dev2.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).put(eamd); + }); + assertForbidden(exception1); + + //Should still be the original owner + amd = client_dev1.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + assertEquals(DEVELOPER_USERNAME, amd.getOwner()); + } + +} diff --git a/common/src/main/resources/META-INF/openapi.json b/common/src/main/resources/META-INF/openapi.json index f8948ad296..92a4189903 100644 --- a/common/src/main/resources/META-INF/openapi.json +++ b/common/src/main/resources/META-INF/openapi.json @@ -4289,6 +4289,7 @@ "type": { "enum": [ "none", + "basic", "oidc" ], "type": "string" diff --git a/docs/modules/ROOT/partials/getting-started/ref-registry-all-configs.adoc b/docs/modules/ROOT/partials/getting-started/ref-registry-all-configs.adoc index 19f38e6284..dd83a0c226 100644 --- a/docs/modules/ROOT/partials/getting-started/ref-registry-all-configs.adoc +++ b/docs/modules/ROOT/partials/getting-started/ref-registry-all-configs.adoc @@ -153,6 +153,11 @@ The following {registry} configuration options are available for each component | |`2.5.0.Final` |Client credentials scope. +|`quarkus.http.auth.basic` +|`boolean` +|`false` +|`3.X.X.Final` +|Enable basic auth |`quarkus.oidc.client-id` |`string` | diff --git a/examples/developer-basic-auth/basic-auth.env b/examples/developer-basic-auth/basic-auth.env new file mode 100644 index 0000000000..c481329538 --- /dev/null +++ b/examples/developer-basic-auth/basic-auth.env @@ -0,0 +1,10 @@ +QUARKUS_OIDC_TENANT_ENABLED=false +QUARKUS_HTTP_AUTH_BASIC=true +APICURIO_AUTH_ROLE_BASED_AUTHORIZATION=true +QUARKUS_SECURITY_USERS_EMBEDDED_PLAIN_TEXT=true +QUARKUS_SECURITY_USERS_EMBEDDED_USERS_admin=admin +QUARKUS_SECURITY_USERS_EMBEDDED_USERS_developer=developer +QUARKUS_SECURITY_USERS_EMBEDDED_USERS_readonly=readonly +QUARKUS_SECURITY_USERS_EMBEDDED_ROLES_admin=sr-admin +QUARKUS_SECURITY_USERS_EMBEDDED_ROLES_developer=sr-developer +QUARKUS_SECURITY_USERS_EMBEDDED_ROLES_readonly=sr-readonly diff --git a/examples/developer-basic-auth/basic-auth.properties b/examples/developer-basic-auth/basic-auth.properties new file mode 100644 index 0000000000..09bd1fde26 --- /dev/null +++ b/examples/developer-basic-auth/basic-auth.properties @@ -0,0 +1,21 @@ +# Disable OIDC +quarkus.oidc.tenant-enabled=false + +# Enable Basic Auth +quarkus.http.auth.basic=true + +# Enable Role Based Auth +apicurio.auth.role-based-authorization=true + +# Enable Testing Embedded Users +quarkus.security.users.embedded.plain-text=true + +# Users configuration +quarkus.security.users.embedded.users.admin=admin +quarkus.security.users.embedded.roles.admin=sr-admin +# example: +# curl -H "Authorization: Basic $(echo -n "admin:admin" | base64)" http://localhost:8080/apis/registry/v2/admin/rules -v +quarkus.security.users.embedded.users.developer=developer +quarkus.security.users.embedded.roles.developer=sr-developer +quarkus.security.users.embedded.users.readonly=readonly +quarkus.security.users.embedded.roles.readonly=sr-readonly diff --git a/pom.xml b/pom.xml index d341a7c5f3..f49848896d 100644 --- a/pom.xml +++ b/pom.xml @@ -190,7 +190,7 @@ 1.2.1.Final 4.5.14 0.1.18.Final - 1.1.0 + 1.2.0 0.15.0 3.6.0 2.2 diff --git a/ui/ui-app/package-lock.json b/ui/ui-app/package-lock.json index a6d5dc4e0c..6b501c7627 100644 --- a/ui/ui-app/package-lock.json +++ b/ui/ui-app/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@apicurio/common-ui-components": "2.0.0", + "@apicurio/common-ui-components": "2.0.1", "@apicurio/data-models": "1.1.27", "@patternfly/patternfly": "5.3.1", "@patternfly/react-code-editor": "5.3.3", @@ -18,6 +18,7 @@ "@patternfly/react-icons": "5.3.2", "@patternfly/react-table": "5.3.3", "axios": "1.6.8", + "buffer": "^6.0.3", "luxon": "3.4.4", "oidc-client-ts": "3.0.1", "react": "18.3.1", @@ -54,9 +55,9 @@ } }, "node_modules/@apicurio/common-ui-components": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@apicurio/common-ui-components/-/common-ui-components-2.0.0.tgz", - "integrity": "sha512-4sKDpZP2+OsvkR1xk5evFlYQb1QnQ80cv//DZdC4zNnLW6Y1pvBzgJ0RUHfTpbwaIbjvG0tQF4YLHkBIBMjjxA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apicurio/common-ui-components/-/common-ui-components-2.0.1.tgz", + "integrity": "sha512-6EBDSc/1baehL97w15mh/HjP93CXV8ncW6DN8GvT53DMIuALjbMEMLweKsasEZ9rgysv6hxq5Wc74aC0QzAm+w==", "peerDependencies": { "@patternfly/patternfly": "~5", "@patternfly/react-core": "~5", @@ -1737,6 +1738,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1770,6 +1790,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2542,6 +2585,25 @@ "node": ">=8" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", diff --git a/ui/ui-app/package.json b/ui/ui-app/package.json index 4fea071bb6..9a5886466a 100644 --- a/ui/ui-app/package.json +++ b/ui/ui-app/package.json @@ -30,14 +30,15 @@ "vite-tsconfig-paths": "4.3.2" }, "dependencies": { + "@apicurio/common-ui-components": "2.0.1", "@apicurio/data-models": "1.1.27", - "@apicurio/common-ui-components": "2.0.0", "@patternfly/patternfly": "5.3.1", "@patternfly/react-code-editor": "5.3.3", "@patternfly/react-core": "5.3.3", "@patternfly/react-icons": "5.3.2", "@patternfly/react-table": "5.3.3", "axios": "1.6.8", + "buffer": "^6.0.3", "luxon": "3.4.4", "oidc-client-ts": "3.0.1", "react": "18.3.1", diff --git a/ui/ui-app/src/app/components/auth/IfAuth.tsx b/ui/ui-app/src/app/components/auth/IfAuth.tsx index cf228e7707..b8d006e3b2 100644 --- a/ui/ui-app/src/app/components/auth/IfAuth.tsx +++ b/ui/ui-app/src/app/components/auth/IfAuth.tsx @@ -33,7 +33,7 @@ export const IfAuth: FunctionComponent = (props: IfAuthProps) => { const accept = () => { let rval: boolean = true; if (props.enabled !== undefined) { - rval = rval && (auth.isAuthEnabled() === props.enabled); + rval = rval && (auth.isOidcAuthEnabled() === props.enabled || auth.isBasicAuthEnabled() === props.enabled); } if (props.isAuthenticated !== undefined) { rval = rval && (isAuthenticated === props.isAuthenticated); diff --git a/ui/ui-app/src/services/useAdminService.ts b/ui/ui-app/src/services/useAdminService.ts index 9fbaa7040b..7e7c818ec4 100644 --- a/ui/ui-app/src/services/useAdminService.ts +++ b/ui/ui-app/src/services/useAdminService.ts @@ -7,9 +7,9 @@ import { DownloadRef } from "@models/downloadRef.model.ts"; import { ConfigurationProperty } from "@models/configurationProperty.model.ts"; import { UpdateConfigurationProperty } from "@models/updateConfigurationProperty.model.ts"; import { + createAuthOptions, createEndpoint, - createHeaders, - createOptions, httpDelete, + httpDelete, httpGet, httpPost, httpPostWithReturn, httpPut, httpPutWithReturn @@ -21,8 +21,7 @@ import { RoleMappingSearchResults } from "@models/roleMappingSearchResults.model const getArtifactTypes = async (config: ConfigService, auth: AuthService): Promise => { console.info("[AdminService] Getting the global list of artifactTypes."); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/config/artifactTypes"); return httpGet(endpoint, options); }; @@ -30,8 +29,7 @@ const getArtifactTypes = async (config: ConfigService, auth: AuthService): Promi const getRules = async (config: ConfigService, auth: AuthService): Promise => { console.info("[AdminService] Getting the global list of rules."); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/rules"); return httpGet(endpoint, options).then( ruleTypes => { return Promise.all(ruleTypes.map(rt => getRule(config, auth, rt))); @@ -40,8 +38,7 @@ const getRules = async (config: ConfigService, auth: AuthService): Promise => { const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/rules/:rule", { rule: type }); @@ -52,8 +49,7 @@ const createRule = async (config: ConfigService, auth: AuthService, type: string console.info("[AdminService] Creating global rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/rules"); const body: Rule = { config: configValue, @@ -66,8 +62,7 @@ const updateRule = async (config: ConfigService, auth: AuthService, type: string console.info("[AdminService] Updating global rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/rules/:rule", { "rule": type }); @@ -79,8 +74,7 @@ const deleteRule = async (config: ConfigService, auth: AuthService, type: string console.info("[AdminService] Deleting global rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/rules/:rule", { "rule": type }); @@ -97,16 +91,14 @@ const getRoleMappings = async (config: ConfigService, auth: AuthService, paging: }; const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/roleMappings", {}, queryParams); return httpGet(endpoint, options); }; const getRoleMapping = async (config: ConfigService, auth: AuthService, principalId: string): Promise => { const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/roleMappings/:principalId", { principalId }); @@ -117,8 +109,7 @@ const createRoleMapping = async (config: ConfigService, auth: AuthService, princ console.info("[AdminService] Creating a role mapping:", principalId, role, principalName); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/roleMappings"); const body: RoleMapping = { principalId, role, principalName }; return httpPost(endpoint, body, options).then(() => { @@ -130,8 +121,7 @@ const updateRoleMapping = async (config: ConfigService, auth: AuthService, princ console.info("[AdminService] Updating role mapping:", principalId, role); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/roleMappings/:principalId", { principalId }); @@ -145,8 +135,7 @@ const deleteRoleMapping = async (config: ConfigService, auth: AuthService, princ console.info("[AdminService] Deleting role mapping for:", principalId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/roleMappings/:principalId", { principalId }); @@ -155,10 +144,12 @@ const deleteRoleMapping = async (config: ConfigService, auth: AuthService, princ const exportAs = async (config: ConfigService, auth: AuthService, filename: string): Promise => { const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const headers: any = createHeaders(token); - headers["Accept"] = "application/zip"; - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); + options.headers = { + ...options.headers, + "Accept": "application/zip" + }; + const endpoint: string = createEndpoint(baseHref, "/admin/export", {}, { forBrowser: true }); @@ -177,10 +168,11 @@ const exportAs = async (config: ConfigService, auth: AuthService, filename: stri const importFrom = async (config: ConfigService, auth: AuthService, file: string | File, progressFunction: (progressEvent: any) => void): Promise => { const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const headers: any = createHeaders(token); - headers["Content-Type"] = "application/zip"; - const options = createOptions(headers); + const options = await createAuthOptions(auth); + options.headers = { + ...options.headers, + "Accept": "application/zip" + }; const endpoint: string = createEndpoint(baseHref, "/admin/import"); return httpPost(endpoint, file, options,undefined, progressFunction); }; @@ -188,8 +180,7 @@ const importFrom = async (config: ConfigService, auth: AuthService, file: string const listConfigurationProperties = async (config: ConfigService, auth: AuthService): Promise => { console.info("[AdminService] Getting the dynamic config properties."); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/config/properties"); return httpGet(endpoint, options); }; @@ -197,8 +188,7 @@ const listConfigurationProperties = async (config: ConfigService, auth: AuthServ const setConfigurationProperty = async (config: ConfigService, auth: AuthService, propertyName: string, newValue: string): Promise => { console.info("[AdminService] Setting a config property: ", propertyName); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/config/properties/:propertyName", { propertyName }); @@ -211,8 +201,7 @@ const setConfigurationProperty = async (config: ConfigService, auth: AuthService const resetConfigurationProperty = async (config: ConfigService, auth: AuthService, propertyName: string): Promise => { console.info("[AdminService] Resetting a config property: ", propertyName); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/admin/config/properties/:propertyName", { propertyName }); diff --git a/ui/ui-app/src/services/useGroupsService.ts b/ui/ui-app/src/services/useGroupsService.ts index 02755f14ad..ae2783766a 100644 --- a/ui/ui-app/src/services/useGroupsService.ts +++ b/ui/ui-app/src/services/useGroupsService.ts @@ -1,8 +1,7 @@ import { ConfigService, useConfigService } from "@services/useConfigService.ts"; import { + createAuthOptions, createEndpoint, - createHeaders, - createOptions, httpDelete, httpGet, httpPostWithReturn, @@ -70,43 +69,62 @@ export interface ClientGeneration { const createArtifact = async (config: ConfigService, auth: AuthService, data: CreateArtifactData): Promise => { const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts", { groupId: data.groupId }); + const options = await createAuthOptions(auth); - const headers: any = createHeaders(token); if (data.id) { - headers["X-Registry-ArtifactId"] = data.id; + options.headers = { + ...options.headers, + "X-Registry-ArtifactId": data.id + }; } if (data.type) { - headers["X-Registry-ArtifactType"] = data.type; + options.headers = { + ...options.headers, + "X-Registry-ArtifactType": data.type + }; } if (data.sha) { - headers["X-Registry-Hash-Algorithm"] = "SHA256"; - headers["X-Registry-Content-Hash"] = data.sha; + options.headers = { + ...options.headers, + "X-Registry-Hash-Algorithm": "SHA256", + "X-Registry-Content-Hash": data.sha + }; } if (data.fromURL) { - headers["Content-Type"] = "application/create.extended+json"; + options.headers = { + ...options.headers, + "Content-Type": "application/create.extended+json" + }; data.content = `{ "content": "${data.fromURL}" }`; } else { - headers["Content-Type"] = contentType(data.type, data.content ? data.content : ""); + options.headers = { + ...options.headers, + "Content-Type": contentType(data.type, data.content ? data.content : "") + }; } - return httpPostWithReturn(endpoint, data.content, createOptions(headers)); + return httpPostWithReturn(endpoint, data.content, options); }; const createArtifactVersion = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, data: CreateVersionData): Promise => { groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); + const options = await createAuthOptions(auth); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }); - const headers: any = createHeaders(token); if (data.type) { - headers["X-Registry-ArtifactType"] = data.type; + options.headers = { + ...options.headers, + "X-Registry-ArtifactType": data.type + }; } - headers["Content-Type"] = contentType(data.type, data.content); - return httpPostWithReturn(endpoint, data.content, createOptions(headers)); + options.headers = { + ...options.headers, + "Content-Type": contentType(data.type, data.content) + }; + return httpPostWithReturn(endpoint, data.content, options); }; const getArtifacts = async (config: ConfigService, auth: AuthService, criteria: GetArtifactsCriteria, paging: Paging): Promise => { @@ -129,9 +147,8 @@ const getArtifacts = async (config: ConfigService, auth: AuthService, criteria: } } const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/search/artifacts", {}, queryParams); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options, (data) => { const results: ArtifactsSearchResults = { artifacts: data.artifacts, @@ -147,9 +164,8 @@ const getArtifactMetaData = async (config: ConfigService, auth: AuthService, gro groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId", { groupId, artifactId }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options); }; @@ -157,11 +173,10 @@ const getArtifactVersionMetaData = async (config: ConfigService, auth: AuthServi groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const versionExpression: string = (version == "latest") ? "branch=latest" : version; const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions/:versionExpression", { groupId, artifactId, versionExpression }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options); }; @@ -170,9 +185,8 @@ const getArtifactReferences = async (config: ConfigService, auth: AuthService, g refType: refType || "OUTBOUND" }; const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/ids/globalIds/:globalId/references", { globalId }, queryParams); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options); }; @@ -184,9 +198,8 @@ const updateArtifactMetaData = async (config: ConfigService, auth: AuthService, groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId", { groupId, artifactId }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpPut(endpoint, metaData, options); }; @@ -194,11 +207,10 @@ const updateArtifactVersionMetaData = async (config: ConfigService, auth: AuthSe groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const versionExpression: string = (version == "latest") ? "branch=latest" : version; const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions/:versionExpression", { groupId, artifactId, versionExpression }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpPut(endpoint, metaData, options); }; @@ -212,14 +224,15 @@ const getArtifactVersionContent = async (config: ConfigService, auth: AuthServic groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const versionExpression: string = (version == "latest") ? "branch=latest" : version; const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions/:versionExpression/content", { groupId, artifactId, versionExpression }); - const headers: any = createHeaders(token); - headers["Accept"] = "*"; - const options = createOptions(headers); + const options = await createAuthOptions(auth); + options.headers = { + ...options.headers, + "Accept": "*" + }; options.maxContentLength = 5242880; // TODO 5MB hard-coded, make this configurable? options.responseType = "text"; options.transformResponse = (data: any) => data; @@ -231,12 +244,11 @@ const getArtifactVersions = async (config: ConfigService, auth: AuthService, gro console.info("[GroupsService] Getting the list of versions for artifact: ", groupId, artifactId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }, { limit: 500, offset: 0 }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options, (data) => { return data.versions; }); @@ -247,9 +259,8 @@ const getArtifactRules = async (config: ConfigService, auth: AuthService, groupI console.info("[GroupsService] Getting the list of rules for artifact: ", groupId, artifactId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/rules", { groupId, artifactId }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options).then( ruleTypes => { return Promise.all(ruleTypes.map(rt => getArtifactRule(config, auth, groupId, artifactId, rt))); }); @@ -259,13 +270,12 @@ const getArtifactRule = async (config: ConfigService, auth: AuthService, groupId groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/rules/:rule", { groupId, artifactId, rule: type }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options); }; @@ -275,13 +285,12 @@ const createArtifactRule = async (config: ConfigService, auth: AuthService, grou console.info("[GroupsService] Creating rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/rules", { groupId, artifactId }); const body: Rule = { config: configValue, type }; - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpPostWithReturn(endpoint, body, options); }; @@ -290,14 +299,13 @@ const updateArtifactRule = async (config: ConfigService, auth: AuthService, grou console.info("[GroupsService] Updating rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/rules/:rule", { groupId, artifactId, "rule": type }); const body: Rule = { config: configValue, type }; - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpPutWithReturn(endpoint, body, options); }; @@ -306,13 +314,12 @@ const deleteArtifactRule = async (config: ConfigService, auth: AuthService, grou console.info("[GroupsService] Deleting rule:", type); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/rules/:rule", { groupId, artifactId, "rule": type }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpDelete(endpoint, options); }; @@ -321,9 +328,8 @@ const deleteArtifact = async (config: ConfigService, auth: AuthService, groupId: console.info("[GroupsService] Deleting artifact:", groupId, artifactId); const baseHref: string = config.artifactsUrl(); - const token: string | undefined = await auth.getToken(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId", { groupId, artifactId }); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpDelete(endpoint, options); }; diff --git a/ui/ui-app/src/services/useUserService.ts b/ui/ui-app/src/services/useUserService.ts index a4e0648fbd..42e4e7b0f6 100644 --- a/ui/ui-app/src/services/useUserService.ts +++ b/ui/ui-app/src/services/useUserService.ts @@ -1,6 +1,6 @@ import { UserInfo } from "@models/userInfo.model.ts"; import { AuthService, useAuth } from "@apicurio/common-ui-components"; -import { createEndpoint, createHeaders, createOptions, httpGet } from "@utils/rest.utils.ts"; +import { createAuthOptions, createEndpoint, httpGet } from "@utils/rest.utils.ts"; import { ConfigService, useConfigService } from "@services/useConfigService.ts"; let currentUserInfo: UserInfo = { @@ -21,8 +21,7 @@ const updateCurrentUser = async (config: ConfigService, auth: AuthService): Prom if (isAuthenticated) { // TODO cache the response for a few minutes to limit the # of times this is called per minute?? const endpoint: string = createEndpoint(config.artifactsUrl(), "/users/me"); - const token: string | undefined = await auth.getToken(); - const options = createOptions(createHeaders(token)); + const options = await createAuthOptions(auth); return httpGet(endpoint, options).then(userInfo => { currentUserInfo = userInfo; return userInfo; @@ -41,7 +40,7 @@ const isObacEnabled = (config: ConfigService): boolean => { }; const isUserAdmin = (config: ConfigService, auth: AuthService): boolean => { - if (!auth.isAuthEnabled()) { + if (!auth.isOidcAuthEnabled() && !auth.isBasicAuthEnabled()) { return true; } if (!isRbacEnabled(config) && !isObacEnabled(config)) { @@ -51,7 +50,7 @@ const isUserAdmin = (config: ConfigService, auth: AuthService): boolean => { }; const isUserDeveloper = (config: ConfigService, auth: AuthService, resourceOwner?: string): boolean => { - if (!auth.isAuthEnabled()) { + if (!auth.isOidcAuthEnabled() && !auth.isBasicAuthEnabled()) { return true; } if (!isRbacEnabled(config) && !isObacEnabled(config)) { diff --git a/ui/ui-app/src/utils/rest.utils.ts b/ui/ui-app/src/utils/rest.utils.ts index d8886d7299..9dbcab27ef 100644 --- a/ui/ui-app/src/utils/rest.utils.ts +++ b/ui/ui-app/src/utils/rest.utils.ts @@ -1,5 +1,7 @@ import axios, { AxiosRequestConfig } from "axios"; import { ContentTypes } from "@models/contentTypes.model.ts"; +import { AuthService } from "@apicurio/common-ui-components"; +import { Buffer } from "buffer"; const AXIOS = axios.create(); @@ -102,6 +104,24 @@ export function createOptions(headers: { [header: string]: string }): AxiosReque return { headers }; } +/** + * Creates the request Auth options used by the HTTP service when making API calls. + * @param auth + */ +export async function createAuthOptions(auth: AuthService): Promise { + if (auth.isOidcAuthEnabled()) { + const token: string | undefined = await auth.getToken(); + return createOptions(createHeaders(token)); + } else if (auth.isBasicAuthEnabled()) { + const creds = auth.getUsernameAndPassword(); + const base64Credentials = Buffer.from(`${creds?.username}:${creds?.password}`, "ascii").toString("base64"); + const headers = { "Authorization": `Basic ${base64Credentials}` }; + return createOptions(headers); + } else { + return Promise.resolve({}); + } +} + /** * Performs an HTTP GET operation to the given URL with the given options. Returns diff --git a/ui/ui-app/vite.config.ts b/ui/ui-app/vite.config.ts index 89d4f61e23..5190954ce8 100644 --- a/ui/ui-app/vite.config.ts +++ b/ui/ui-app/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ plugins: [react(), tsconfigPaths()], server: { port: PORT - } + }, // define: { // "process.platform": {} // } diff --git a/utils/tests/src/main/java/io/apicurio/registry/utils/tests/BasicAuthWithPropertiesTestProfile.java b/utils/tests/src/main/java/io/apicurio/registry/utils/tests/BasicAuthWithPropertiesTestProfile.java new file mode 100644 index 0000000000..e9fc6bf569 --- /dev/null +++ b/utils/tests/src/main/java/io/apicurio/registry/utils/tests/BasicAuthWithPropertiesTestProfile.java @@ -0,0 +1,37 @@ +package io.apicurio.registry.utils.tests; + +import io.quarkus.test.junit.QuarkusTestProfile; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BasicAuthWithPropertiesTestProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + Map map = new HashMap<>(); + map.put("quarkus.oidc.tenant-enabled", "false"); + map.put("quarkus.http.auth.basic", "true"); + map.put("apicurio.auth.admin-override.enabled", "true"); + map.put("apicurio.auth.role-based-authorization", "true"); + map.put("apicurio.auth.owner-only-authorization", "true"); + map.put("quarkus.security.users.embedded.enabled", "true"); + map.put("quarkus.security.users.embedded.plain-text", "true"); + map.put("quarkus.security.users.embedded.users.alice", "alice"); + map.put("quarkus.security.users.embedded.users.bob1", "bob1"); + map.put("quarkus.security.users.embedded.users.bob2", "bob2"); + map.put("quarkus.security.users.embedded.users.duncan", "duncan"); + map.put("quarkus.security.users.embedded.roles.alice", "sr-admin"); + map.put("quarkus.security.users.embedded.roles.bob1", "sr-developer"); + map.put("quarkus.security.users.embedded.roles.bob2", "sr-developer"); + map.put("quarkus.security.users.embedded.roles.duncan", "sr-readonly"); + return map; + } + + @Override + public List testResources() { + return Collections.emptyList(); + } +}