diff --git a/build.gradle.kts b/build.gradle.kts index e52ffba..60d6fa3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,8 @@ repositories { dependencies { implementation("dev.openfga:openfga-sdk:0.2.2") + implementation("org.dmfs:oauth2-essentials:0.22.0") + implementation("org.dmfs:httpurlconnection-executor:1.21.3") } diff --git a/src/main/java/com/github/le_yams/openfga4intellij/actions/DslToJsonAction.java b/src/main/java/com/github/le_yams/openfga4intellij/actions/DslToJsonAction.java index dd3cc0e..7451751 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/actions/DslToJsonAction.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/actions/DslToJsonAction.java @@ -6,15 +6,12 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressManager; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; public class DslToJsonAction extends AnAction { - private static final Logger logger = Logger.getInstance(DslToJsonAction.class); - @Override public void update(@NotNull AnActionEvent event) { super.update(event); diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/ServersUtil.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/ServersUtil.java deleted file mode 100644 index cde67fd..0000000 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/ServersUtil.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.le_yams.openfga4intellij.servers; - -import com.github.le_yams.openfga4intellij.servers.model.Server; -import com.intellij.openapi.diagnostic.Logger; -import dev.openfga.sdk.api.client.ClientListStoresResponse; -import dev.openfga.sdk.api.client.OpenFgaClient; -import dev.openfga.sdk.api.configuration.*; -import dev.openfga.sdk.errors.FgaInvalidParameterException; - -import java.util.concurrent.CompletableFuture; - -public class ServersUtil { - - private static final Logger logger = Logger.getInstance(ServersUtil.class); - - public static CompletableFuture testConnection(Server server) throws FgaInvalidParameterException { - var apiUrl = server.loadUrl(); - logger.warn("testing connection to " + apiUrl + " with authentication method: " + server.getAuthenticationMethod()); - - var config = new ClientConfiguration().apiUrl(apiUrl); - switch (server.getAuthenticationMethod()) { - case API_TOKEN -> { - var token = server.loadApiToken(); - config = config.credentials(new Credentials( - new ApiToken(token) - )); - } - case OIDC -> { - var oidc = server.loadOidc(); - config = config.credentials(new Credentials( - new ClientCredentials() - .apiTokenIssuer(oidc.issuer()) - .apiAudience(oidc.audience()) - .clientId(oidc.clientId()) - .clientSecret(oidc.clientSecret()) - )); - } - } - - var fgaClient = new OpenFgaClient(config); - - var options = new ClientListStoresOptions() - .pageSize(1); - - var stores = fgaClient.listStores(options); - - return stores.thenApply(ClientListStoresResponse::getStatusCode); - } -} diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Oidc.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Oidc.java index 6280f8f..97c0982 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Oidc.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Oidc.java @@ -1,9 +1,9 @@ package com.github.le_yams.openfga4intellij.servers.model; public record Oidc( + String authority, String clientId, String clientSecret, - String issuer, - String audience + String scope ) { } diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Server.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Server.java index dd35c8d..622f370 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Server.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/model/Server.java @@ -23,7 +23,7 @@ public Server(String name) { } public String loadUrl() { - var credentials = getCredentials("url"); + var credentials = getCredentials(CredentialKey.URL); if (credentials == null) { return ""; } @@ -31,12 +31,12 @@ public String loadUrl() { } public void storeUrl(String url) { - var attributes = getCredentialAttributes("url"); + var attributes = getCredentialAttributes(CredentialKey.URL); PasswordSafe.getInstance().set(attributes, new Credentials(id, url)); } public String loadApiToken() { - var credentials = getCredentials("apiToken"); + var credentials = getCredentials(CredentialKey.API_TOKEN); if (credentials == null) { return ""; } @@ -44,7 +44,7 @@ public String loadApiToken() { } public void storeApiToken(String token) { - var attributes = getCredentialAttributes("apiToken"); + var attributes = getCredentialAttributes(CredentialKey.API_TOKEN); PasswordSafe.getInstance().set(attributes, new Credentials(id, token)); } @@ -84,26 +84,26 @@ public void setAuthenticationMethod(AuthenticationMethod authenticationMethod) { } public Oidc loadOidc() { - var credentials = getCredentials("oidc_client"); + var credentials = getCredentials(CredentialKey.OIDC_CLIENT); var clientId = credentials != null ? credentials.getUserName() : ""; var clientSecret = credentials != null ? credentials.getPasswordAsString() : ""; - credentials = getCredentials("oidc_issuer"); - var issuer = credentials != null ? credentials.getPasswordAsString() : ""; + credentials = getCredentials(CredentialKey.OIDC_AUTHORITY); + var authority = credentials != null ? credentials.getPasswordAsString() : ""; - credentials = getCredentials("oidc_audience"); + credentials = getCredentials(CredentialKey.OIDC_SCOPE); var audience = credentials != null ? credentials.getPasswordAsString() : ""; - return new Oidc(clientId, clientSecret, issuer, audience); + return new Oidc(authority, clientId, clientSecret, audience); } public void storeOidc(Oidc oidc) { - var attributes = getCredentialAttributes("oidc_client"); + var attributes = getCredentialAttributes(CredentialKey.OIDC_CLIENT); PasswordSafe.getInstance().set(attributes, new Credentials(oidc.clientId(), oidc.clientSecret())); - attributes = getCredentialAttributes("oidc_issuer"); - PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.issuer())); - attributes = getCredentialAttributes("oidc_audience"); - PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.audience())); + attributes = getCredentialAttributes(CredentialKey.OIDC_AUTHORITY); + PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.authority())); + attributes = getCredentialAttributes(CredentialKey.OIDC_SCOPE); + PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.scope())); } @Override @@ -111,4 +111,12 @@ public String toString() { return name; } + private interface CredentialKey { + + String URL = "url"; + String API_TOKEN = "apiToken"; + String OIDC_CLIENT = "oidc_client"; + String OIDC_AUTHORITY = "oidc_authority"; + String OIDC_SCOPE = "oidc_scope"; + } } diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowContent.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowContent.java index 6bb96b9..2773013 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowContent.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowContent.java @@ -2,6 +2,7 @@ import com.github.le_yams.openfga4intellij.servers.service.OpenFGAServers; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.wm.ToolWindow; import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.treeStructure.Tree; import org.jetbrains.annotations.NotNull; @@ -14,8 +15,13 @@ import java.util.Optional; class OpenFGAToolWindowContent { + private final ToolWindow toolWindow; private Tree tree; + OpenFGAToolWindowContent(ToolWindow toolWindow) { + this.toolWindow = toolWindow; + } + public JComponent getContentPanel() { var mainPanel = new JPanel(new BorderLayout()); @@ -27,7 +33,7 @@ public JComponent getContentPanel() { toolbarDecorator.setAddAction(anActionButton -> { - var server = ServerDialog.showAddServerDialog(); + var server = ServerDialog.showAddServerDialog(toolWindow); if (server == null) { return; } @@ -44,7 +50,7 @@ public JComponent getContentPanel() { toolbarDecorator.setEditActionUpdater(updater -> getSelectedNode().isPresent()); toolbarDecorator.setEditAction(anActionButton -> getSelectedNode() .ifPresent(node -> { - ServerDialog.showEditServerDialog(node.getServer()); + ServerDialog.showEditServerDialog(toolWindow, node.getServer()); tree.updateUI(); })); toolbarDecorator.setRemoveActionUpdater(updater -> getSelectedNode().isPresent()); diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowFactory.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowFactory.java index 4f42fef..c56a64c 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowFactory.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/OpenFGAToolWindowFactory.java @@ -11,7 +11,7 @@ public class OpenFGAToolWindowFactory implements ToolWindowFactory { @Override public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { - var toolWindowContent = new OpenFGAToolWindowContent(); + var toolWindowContent = new OpenFGAToolWindowContent(toolWindow); Content content = ContentFactory.getInstance().createContent(toolWindowContent.getContentPanel(), "", false); toolWindow.getContentManager().addContent(content); } diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/ServerDialog.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/ServerDialog.java index 5035209..205067e 100644 --- a/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/ServerDialog.java +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/ui/ServerDialog.java @@ -1,11 +1,19 @@ package com.github.le_yams.openfga4intellij.servers.ui; +import com.github.le_yams.openfga4intellij.servers.util.ServersUtil; import com.github.le_yams.openfga4intellij.servers.model.AuthenticationMethod; import com.github.le_yams.openfga4intellij.servers.model.Oidc; import com.github.le_yams.openfga4intellij.servers.model.Server; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.ui.DialogPanel; -import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.ui.*; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.ui.AnimatedIcon; +import com.intellij.ui.components.ActionLink; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBPasswordField; import com.intellij.ui.components.JBTextField; @@ -18,23 +26,29 @@ import java.awt.event.ItemEvent; public class ServerDialog extends DialogWrapper { + private static final Logger logger = Logger.getInstance(com.github.le_yams.openfga4intellij.servers.ui.ServerDialog.class); + private final ToolWindow toolWindow; private final Server server; + private DialogPanel dialogPanel; private final JBTextField nameField = new JBTextField(); private final JBTextField urlField = new JBTextField(); private final ComboBox authenticationMethodField = new ComboBox<>(AuthenticationMethod.values()); private final JBPasswordField apiTokenField = new JBPasswordField(); private final JBTextField oidcClientIdField = new JBTextField(); private final JBPasswordField oidcClientSecretField = new JBPasswordField(); - private final JBTextField oidcIssuerField = new JBTextField(); - private final JBTextField oidcAudienceField = new JBTextField(); + private final JBTextField oidcAuthorityField = new JBTextField(); + private final JBTextField oidcScopeField = new JBTextField(); + private final ActionLink connectionTestButton = new ActionLink("Test connexion"); + private final JBLabel connectionTestLabel = new JBLabel(); - protected ServerDialog() { - this(null); + protected ServerDialog(ToolWindow toolWindow) { + this(toolWindow, null); } - public ServerDialog(@Nullable Server server) { + public ServerDialog(ToolWindow toolWindow, @Nullable Server server) { super(true); + this.toolWindow = toolWindow; this.server = server != null ? server : new Server(); setTitle(server != null ? "Edit Server" : "Add Server"); init(); @@ -42,7 +56,7 @@ public ServerDialog(@Nullable Server server) { @Override protected @Nullable JComponent createCenterPanel() { - DialogPanel dialogPanel = new DialogPanel(new MigLayout("fillx,wrap 2", "[left]rel[grow,fill]")); + dialogPanel = new DialogPanel(new MigLayout("fillx,wrap 2", "[left]rel[grow,fill]")); dialogPanel.add(new JBLabel("Name")); dialogPanel.add(nameField); @@ -55,6 +69,19 @@ public ServerDialog(@Nullable Server server) { dialogPanel.add(createAuthenticationPanel(), "span, grow"); + var connectionTestPanel = new JPanel(new BorderLayout(3, 0)); + + connectionTestPanel.add(connectionTestButton, BorderLayout.WEST); + connectionTestPanel.add(connectionTestLabel, BorderLayout.CENTER); + dialogPanel.add(connectionTestPanel, "span, grow"); + + connectionTestButton.addActionListener(evt -> { + var testServer = writeToModel(new Server()); + connectionTestLabel.setText("testing connection with " + testServer.loadUrl()); + connectionTestLabel.setIcon(new AnimatedIcon.Default()); + ProgressManager.getInstance().run(new com.github.le_yams.openfga4intellij.servers.ui.ServerDialog.ConnectionTestTask(testServer)); + }); + loadModel(); return dialogPanel; @@ -96,17 +123,17 @@ private JPanel createApiTokenPanel() { private JPanel createOidcPanel() { var oidcPanel = new JPanel(new MigLayout("fillx,wrap 2", "[left]rel[grow,fill]")); + oidcPanel.add(new JBLabel("Authority")); + oidcPanel.add(oidcAuthorityField); + oidcPanel.add(new JBLabel("Client id")); oidcPanel.add(oidcClientIdField); oidcPanel.add(new JBLabel("Client secret")); oidcPanel.add(oidcClientSecretField); - oidcPanel.add(new JBLabel("Issuer")); - oidcPanel.add(oidcIssuerField); - - oidcPanel.add(new JBLabel("Audience")); - oidcPanel.add(oidcAudienceField); + oidcPanel.add(new JBLabel("Scope")); + oidcPanel.add(oidcScopeField); return oidcPanel; } @@ -124,8 +151,8 @@ private void loadModel() { var oidc = server.loadOidc(); oidcClientIdField.setText(oidc.clientId()); oidcClientSecretField.setText(oidc.clientSecret()); - oidcIssuerField.setText(oidc.issuer()); - oidcAudienceField.setText(oidc.audience()); + oidcAuthorityField.setText(oidc.authority()); + oidcScopeField.setText(oidc.scope()); } @@ -143,23 +170,82 @@ private Server writeToModel(Server server) { } case API_TOKEN -> server.storeApiToken(new String(apiTokenField.getPassword())); case OIDC -> server.storeOidc(new Oidc( - oidcClientIdField.getText(), + oidcAuthorityField.getText(), oidcClientIdField.getText(), new String(oidcClientSecretField.getPassword()), - oidcIssuerField.getText(), - oidcAudienceField.getText() + oidcScopeField.getText() )); } return server; } - public static Server showAddServerDialog() { - var dialog = new ServerDialog(); + public static Server showAddServerDialog(ToolWindow toolWindow) { + var dialog = new ServerDialog(toolWindow); return dialog.showAndGet() ? dialog.updateModel() : null; } - public static Server showEditServerDialog(Server server) { - var dialog = new ServerDialog(server); + public static Server showEditServerDialog(ToolWindow toolWindow, Server server) { + var dialog = new ServerDialog(toolWindow, server); return dialog.showAndGet() ? dialog.updateModel() : null; } + + + private class ConnectionTestTask extends Task.Backgroundable { + private final Server testServer; + + public ConnectionTestTask(Server testServer) { + super(toolWindow.getProject(), "Connection test", false); + this.testServer = testServer; + } + + public void run(@NotNull ProgressIndicator progressIndicator) { + progressIndicator.setIndeterminate(true); + progressIndicator.setText(connectionTestLabel.getText()); + try { + var httpStatus = ServersUtil.testConnection(testServer); + httpStatus.whenCompleteAsync((httpStatusValue, throwable) -> { + if (throwable != null) { + taskFailed(throwable); + } else if (httpStatusValue < 300) { + taskSucceeded(); + } else { + taskFailed("Openfga server connection test filed with HTTP statuc " + httpStatusValue); + } + }); + } catch (Exception exception) { + taskFailed(exception); + } finally { + progressIndicator.setFraction(1); + progressIndicator.setIndeterminate(false); + } + } + + private void taskFailed(String errorMessage) { + taskFailed(errorMessage, null); + } + private void taskFailed(Throwable throwable) { + taskFailed(throwable.getMessage(), throwable); + } + private void taskFailed(String errorMessage, Throwable throwable) { + SwingUtilities.invokeLater(() -> { + connectionTestLabel.setText(testServer + " connection test failed"); + connectionTestLabel.setIcon(AllIcons.General.ErrorDialog); + connectionTestLabel.setIcon(AllIcons.RunConfigurations.TestError); + }); + if (throwable != null) { + logger.warn(errorMessage, throwable); + } + ToolWindowManager + .getInstance(toolWindow.getProject()) + .notifyByBalloon(toolWindow.getId(), MessageType.ERROR, errorMessage); + } + + private void taskSucceeded() { + SwingUtilities.invokeLater(() -> { + connectionTestLabel.setText(testServer + " connection test succeeded"); + connectionTestLabel.setIcon(AllIcons.RunConfigurations.TestPassed); + }); + } + + } } diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcException.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcException.java new file mode 100644 index 0000000..f1b823a --- /dev/null +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcException.java @@ -0,0 +1,12 @@ +package com.github.le_yams.openfga4intellij.servers.util; + +public class OidcException extends Exception { + + public OidcException(String message) { + super(message); + } + + public OidcException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcUtil.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcUtil.java new file mode 100644 index 0000000..8ea2168 --- /dev/null +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/OidcUtil.java @@ -0,0 +1,104 @@ +package com.github.le_yams.openfga4intellij.servers.util; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.le_yams.openfga4intellij.servers.model.Oidc; +import org.dmfs.httpessentials.exceptions.ProtocolError; +import org.dmfs.httpessentials.exceptions.ProtocolException; +import org.dmfs.httpessentials.httpurlconnection.HttpUrlConnectionExecutor; +import org.dmfs.oauth2.client.BasicOAuth2AuthorizationProvider; +import org.dmfs.oauth2.client.BasicOAuth2Client; +import org.dmfs.oauth2.client.BasicOAuth2ClientCredentials; +import org.dmfs.oauth2.client.grants.ClientCredentialsGrant; +import org.dmfs.oauth2.client.scope.BasicScope; +import org.dmfs.rfc5545.Duration; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static java.time.temporal.ChronoUnit.SECONDS; + +class OidcUtil { + + private static final String DISCOVERY_DOCUMENT = "/.well-known/openid-configuration"; + + static String getAccessToken(Oidc oidc) throws OidcException { + var tokenEndpoint = getTokenEndpoint(oidc); + return createAccessToken(tokenEndpoint, oidc); + } + + private static String getTokenEndpoint(Oidc oidc) throws OidcException { + var authorityUri = URI.create(oidc.authority()); + + var documentUri = UriUtil.resolve(authorityUri, DISCOVERY_DOCUMENT); + var request = HttpRequest.newBuilder() + .GET() + .uri(documentUri) + .header("accept", "application/json") + .timeout(java.time.Duration.of(10, SECONDS)) + .build(); + + var client = HttpClient + .newBuilder() + .build(); + + try { + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() > 299) { + throw new OidcException("failed to retrieve discovery document at " + documentUri + " with http code" + response.statusCode()); + } + + var discoveryDocument = new ObjectMapper() + .readValue(response.body(), DiscoveryDocument.class); + + return discoveryDocument.getTokenEndpoint(); + } catch (IOException | InterruptedException e) { + throw new OidcException("failed to retrieve discovery document at " + documentUri + ":" + e.getMessage(), e); + } + } + + private static String createAccessToken(String tokenEndpoint, Oidc oidc) throws OidcException { + var provider = new BasicOAuth2AuthorizationProvider( + URI.create("dummy"), + URI.create(tokenEndpoint), + new Duration(1, 0, 3600)); + + var credentials = new BasicOAuth2ClientCredentials( + oidc.clientId(), oidc.clientSecret()); + + var client = new BasicOAuth2Client( + provider, + credentials, + (URI) null); + + var executor = new HttpUrlConnectionExecutor(); + try { + var token = new ClientCredentialsGrant(client, new BasicScope(oidc.scope())).accessToken(executor); + return String.valueOf(token.accessToken()); + } catch (IOException | ProtocolError | ProtocolException e) { + throw new OidcException("failed to retrieve access token: " + e.getMessage(), e); + } + } + + + @JsonIgnoreProperties(ignoreUnknown = true) + static class DiscoveryDocument { + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + } + +} diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServerConnectionException.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServerConnectionException.java new file mode 100644 index 0000000..34ee024 --- /dev/null +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServerConnectionException.java @@ -0,0 +1,8 @@ +package com.github.le_yams.openfga4intellij.servers.util; + +public class ServerConnectionException extends Exception { + + public ServerConnectionException(Throwable cause) { + super(cause.getMessage(), cause); + } +} diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServersUtil.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServersUtil.java new file mode 100644 index 0000000..341b995 --- /dev/null +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/ServersUtil.java @@ -0,0 +1,76 @@ +package com.github.le_yams.openfga4intellij.servers.util; + +import com.github.le_yams.openfga4intellij.servers.model.Oidc; +import com.github.le_yams.openfga4intellij.servers.model.Server; +import dev.openfga.sdk.api.client.ClientListStoresResponse; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ApiToken; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import dev.openfga.sdk.api.configuration.ClientListStoresOptions; +import dev.openfga.sdk.api.configuration.Credentials; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static java.time.temporal.ChronoUnit.SECONDS; + + +public class ServersUtil { + + public static CompletableFuture testConnection(Server server) throws ServerConnectionException { + var apiUrl = server.loadUrl(); + return switch (server.getAuthenticationMethod()) { + case OIDC -> testOidcConnection(server.loadOidc(), apiUrl); // openfga sdk oidc implementation doesn't work properly + case NONE -> testListStoresWithOpenFgaClient(apiUrl); + case API_TOKEN -> testListStoresWithOpenFgaClient(apiUrl, Optional.of(server.loadApiToken())); + }; + } + + private static CompletableFuture testListStoresWithOpenFgaClient(String apiUrl) throws ServerConnectionException { + return testListStoresWithOpenFgaClient(apiUrl, Optional.empty()); + } + + private static CompletableFuture testListStoresWithOpenFgaClient(String apiUrl, Optional apiToken) throws ServerConnectionException { + var config = new ClientConfiguration().apiUrl(apiUrl); + apiToken.ifPresent(token -> + config.credentials(new Credentials(new ApiToken(token)))); + + try { + var fgaClient = new OpenFgaClient(config); + var options = new ClientListStoresOptions() + .pageSize(1); + var stores = fgaClient.listStores(options); + return stores.thenApply(ClientListStoresResponse::getStatusCode); + } catch (Exception e) { + throw new ServerConnectionException(e); + } + } + + private static CompletableFuture testOidcConnection(Oidc oidc, String apiUrl) throws ServerConnectionException { + try { + var accessToken = OidcUtil.getAccessToken(oidc); + return testListStoresWithAccessToken(apiUrl, accessToken); + } catch (Exception e) { + throw new ServerConnectionException(e); + } + } + + private static CompletableFuture testListStoresWithAccessToken(String apiUrl, String accessToken) { + var request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(apiUrl).resolve("/stores?page_size=1")) + .header("accept", "application/json") + .header("Authorization", "Bearer " + accessToken) + .timeout(java.time.Duration.of(10, SECONDS)) + .build(); + + return HttpClient.newBuilder().build() + .sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .thenApply(HttpResponse::statusCode); + } + +} diff --git a/src/main/java/com/github/le_yams/openfga4intellij/servers/util/UriUtil.java b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/UriUtil.java new file mode 100644 index 0000000..b78ca54 --- /dev/null +++ b/src/main/java/com/github/le_yams/openfga4intellij/servers/util/UriUtil.java @@ -0,0 +1,16 @@ +package com.github.le_yams.openfga4intellij.servers.util; + +import java.net.URI; + +public class UriUtil { + + public static URI resolve(URI baseUri, String path) { + var normalizedBaseUri = new StringBuilder(baseUri.toString()); + + if (!baseUri.toString().endsWith("/")) { + normalizedBaseUri.append('/'); + } + + return URI.create(normalizedBaseUri.toString()).resolve(path); + } +}