From bf286b9e57b778a9c0599547321d3e58a2530af1 Mon Sep 17 00:00:00 2001 From: Colt McNealy Date: Mon, 25 Nov 2024 07:46:00 -0800 Subject: [PATCH 1/2] fix(server): fixes mtls authentication (#1156) - properly inspect client certificate to determine principal id - refactor grpc server interceptors to be more clear - refactor boundaries between authorization and authentication --- local-dev/issue-certificates.sh | 2 +- .../io/littlehorse/common/LHServerConfig.java | 23 +++--- .../java/io/littlehorse/server/LHServer.java | 6 +- .../littlehorse/server/LHServerListener.java | 2 +- .../server/auth/InsecureServerAuthorizer.java | 19 ----- ...thorizer.java => LHServerInterceptor.java} | 7 +- .../server/auth/MTLSServerAuthorizer.java | 29 -------- .../server/auth/RequestAuthorizer.java | 2 +- .../server/auth/RequestSanitizer.java | 18 ----- .../authenticators/InsecureAuthenticator.java | 26 +++++++ .../authenticators/MTLSAuthenticator.java | 73 +++++++++++++++++++ .../OAuthAuthenticator.java} | 11 ++- .../{ => authenticators}/OAuthClient.java | 4 +- .../InternalAuthorizer.java | 12 ++- .../InternalCallCredentials.java | 8 +- .../server/listener/ServerListenerConfig.java | 16 ++-- .../server/streams/BackendInternalComms.java | 4 +- .../core/ProcessorExecutionContext.java | 2 +- ...st.java => InsecureAuthenticatorTest.java} | 31 +++++--- .../server/auth/RequestAuthorizerTest.java | 30 ++++---- .../test/internal/TestBootstrapper.java | 4 +- 21 files changed, 198 insertions(+), 131 deletions(-) delete mode 100644 server/src/main/java/io/littlehorse/server/auth/InsecureServerAuthorizer.java rename server/src/main/java/io/littlehorse/server/auth/{ServerAuthorizer.java => LHServerInterceptor.java} (65%) delete mode 100644 server/src/main/java/io/littlehorse/server/auth/MTLSServerAuthorizer.java delete mode 100644 server/src/main/java/io/littlehorse/server/auth/RequestSanitizer.java create mode 100644 server/src/main/java/io/littlehorse/server/auth/authenticators/InsecureAuthenticator.java create mode 100644 server/src/main/java/io/littlehorse/server/auth/authenticators/MTLSAuthenticator.java rename server/src/main/java/io/littlehorse/server/auth/{OAuthServerAuthenticator.java => authenticators/OAuthAuthenticator.java} (89%) rename server/src/main/java/io/littlehorse/server/auth/{ => authenticators}/OAuthClient.java (95%) rename server/src/main/java/io/littlehorse/server/auth/{ => internalport}/InternalAuthorizer.java (82%) rename server/src/main/java/io/littlehorse/server/auth/{ => internalport}/InternalCallCredentials.java (85%) rename server/src/test/java/io/littlehorse/server/auth/{RequestSanitizerTest.java => InsecureAuthenticatorTest.java} (66%) diff --git a/local-dev/issue-certificates.sh b/local-dev/issue-certificates.sh index 0ba49a1a26..25668f7f3f 100755 --- a/local-dev/issue-certificates.sh +++ b/local-dev/issue-certificates.sh @@ -51,7 +51,7 @@ echo "Creating Client Certificates" openssl req -newkey rsa:2048 -nodes \ -out "$CLIENT_PATH/client.csr" \ -keyout "$CLIENT_PATH/client.key" \ - -subj "/CN=localhost/O=client organization" \ + -subj "/CN=obiwan/O=client organization" \ -addext "subjectAltName = DNS:localhost" > /dev/null 2>&1 openssl x509 -req -sha256 -days 3650 \ -CA "$CA_PATH/ca.crt" \ diff --git a/server/src/main/java/io/littlehorse/common/LHServerConfig.java b/server/src/main/java/io/littlehorse/common/LHServerConfig.java index 4caff8eb5a..cece0e29bf 100644 --- a/server/src/main/java/io/littlehorse/common/LHServerConfig.java +++ b/server/src/main/java/io/littlehorse/common/LHServerConfig.java @@ -1060,17 +1060,12 @@ public ServerCredentials getInternalServerCreds() { String caCertFile = getOrSetDefault(INTERNAL_CA_CERT_KEY, null); String serverCertFile = getOrSetDefault(INTERNAL_SERVER_CERT_KEY, null); String serverKeyFile = getOrSetDefault(INTERNAL_SERVER_KEY_KEY, null); - return getCreds(caCertFile, serverCertFile, serverKeyFile); - } - public ChannelCredentials getInternalClientCreds() { - String caCertFile = getOrSetDefault(INTERNAL_CA_CERT_KEY, null); - String serverCertFile = getOrSetDefault(INTERNAL_SERVER_CERT_KEY, null); - String serverKeyFile = getOrSetDefault(INTERNAL_SERVER_KEY_KEY, null); if (caCertFile == null) { - log.info("No ca cert file, using plaintext internal client"); + log.info("No ca cert file found, deploying insecure!"); return null; } + if (serverCertFile == null || serverKeyFile == null) { throw new LHMisconfigurationException("CA cert file provided but missing cert or key"); } @@ -1079,21 +1074,24 @@ public ChannelCredentials getInternalClientCreds() { File rootCA = new File(caCertFile); try { - return TlsChannelCredentials.newBuilder() + return TlsServerCredentials.newBuilder() .keyManager(serverCert, serverKey) .trustManager(rootCA) + .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) .build(); } catch (IOException exn) { throw new RuntimeException(exn); } } - private ServerCredentials getCreds(String caCertFile, String serverCertFile, String serverKeyFile) { + public ChannelCredentials getInternalClientCreds() { + String caCertFile = getOrSetDefault(INTERNAL_CA_CERT_KEY, null); + String serverCertFile = getOrSetDefault(INTERNAL_SERVER_CERT_KEY, null); + String serverKeyFile = getOrSetDefault(INTERNAL_SERVER_KEY_KEY, null); if (caCertFile == null) { - log.info("No ca cert file found, deploying insecure!"); + log.info("No ca cert file, using plaintext internal client"); return null; } - if (serverCertFile == null || serverKeyFile == null) { throw new LHMisconfigurationException("CA cert file provided but missing cert or key"); } @@ -1102,10 +1100,9 @@ private ServerCredentials getCreds(String caCertFile, String serverCertFile, Str File rootCA = new File(caCertFile); try { - return TlsServerCredentials.newBuilder() + return TlsChannelCredentials.newBuilder() .keyManager(serverCert, serverKey) .trustManager(rootCA) - .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) .build(); } catch (IOException exn) { throw new RuntimeException(exn); diff --git a/server/src/main/java/io/littlehorse/server/LHServer.java b/server/src/main/java/io/littlehorse/server/LHServer.java index 13aaae9574..9d450ddb4e 100644 --- a/server/src/main/java/io/littlehorse/server/LHServer.java +++ b/server/src/main/java/io/littlehorse/server/LHServer.java @@ -20,9 +20,8 @@ import io.littlehorse.sdk.common.exception.LHMisconfigurationException; import io.littlehorse.sdk.common.proto.LHHostInfo; import io.littlehorse.sdk.common.proto.PollTaskResponse; -import io.littlehorse.server.auth.InternalCallCredentials; import io.littlehorse.server.auth.RequestAuthorizer; -import io.littlehorse.server.auth.RequestSanitizer; +import io.littlehorse.server.auth.internalport.InternalCallCredentials; import io.littlehorse.server.listener.ServerListenerConfig; import io.littlehorse.server.monitoring.HealthService; import io.littlehorse.server.streams.BackendInternalComms; @@ -121,8 +120,7 @@ private LHServerListener createListener(ServerListenerConfig listenerConfig) { List.of( new MetricCollectingServerInterceptor(healthService.getMeterRegistry()), new RequestAuthorizer(contextKey, metadataCache, coreStoreProvider, config), - listenerConfig.getServerAuthorizer(), - new RequestSanitizer()), + listenerConfig.getRequestAuthenticator()), contextKey); } diff --git a/server/src/main/java/io/littlehorse/server/LHServerListener.java b/server/src/main/java/io/littlehorse/server/LHServerListener.java index 3cf8b6aad8..c344c44005 100644 --- a/server/src/main/java/io/littlehorse/server/LHServerListener.java +++ b/server/src/main/java/io/littlehorse/server/LHServerListener.java @@ -200,7 +200,7 @@ import io.littlehorse.sdk.common.proto.WorkflowEventId; import io.littlehorse.sdk.common.proto.WorkflowEventIdList; import io.littlehorse.sdk.common.proto.WorkflowEventList; -import io.littlehorse.server.auth.InternalCallCredentials; +import io.littlehorse.server.auth.internalport.InternalCallCredentials; import io.littlehorse.server.listener.ServerListenerConfig; import io.littlehorse.server.streams.BackendInternalComms; import io.littlehorse.server.streams.lhinternalscan.PublicScanReply; diff --git a/server/src/main/java/io/littlehorse/server/auth/InsecureServerAuthorizer.java b/server/src/main/java/io/littlehorse/server/auth/InsecureServerAuthorizer.java deleted file mode 100644 index 035fb20594..0000000000 --- a/server/src/main/java/io/littlehorse/server/auth/InsecureServerAuthorizer.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.littlehorse.server.auth; - -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCall.Listener; -import io.grpc.ServerCallHandler; - -public class InsecureServerAuthorizer implements ServerAuthorizer { - - @Override - public Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - return next.startCall(call, headers); - } - - public static InsecureServerAuthorizer create() { - return new InsecureServerAuthorizer(); - } -} diff --git a/server/src/main/java/io/littlehorse/server/auth/ServerAuthorizer.java b/server/src/main/java/io/littlehorse/server/auth/LHServerInterceptor.java similarity index 65% rename from server/src/main/java/io/littlehorse/server/auth/ServerAuthorizer.java rename to server/src/main/java/io/littlehorse/server/auth/LHServerInterceptor.java index 1fe9df634b..cff92fda3b 100644 --- a/server/src/main/java/io/littlehorse/server/auth/ServerAuthorizer.java +++ b/server/src/main/java/io/littlehorse/server/auth/LHServerInterceptor.java @@ -4,7 +4,12 @@ import io.grpc.ServerInterceptor; import io.littlehorse.common.LHConstants; -public interface ServerAuthorizer extends ServerInterceptor { +/** + * Wrapper over io.grpc.ServerInterceptor. Doesn't do anything other than put + * a few constants into scope without imports in order to reduce the scope of + * how we expose the `Metadata.Key` stuff. + */ +public interface LHServerInterceptor extends ServerInterceptor { String INTERNAL_PREFIX = "_"; diff --git a/server/src/main/java/io/littlehorse/server/auth/MTLSServerAuthorizer.java b/server/src/main/java/io/littlehorse/server/auth/MTLSServerAuthorizer.java deleted file mode 100644 index 4ebd86e477..0000000000 --- a/server/src/main/java/io/littlehorse/server/auth/MTLSServerAuthorizer.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.littlehorse.server.auth; - -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCall.Listener; -import io.grpc.ServerCallHandler; -import io.littlehorse.common.util.CertificateUtil; -import io.littlehorse.server.listener.MTLSConfig; - -public class MTLSServerAuthorizer implements ServerAuthorizer { - - private final String commonName; - - public MTLSServerAuthorizer(MTLSConfig mtlsConfig) { - try { - this.commonName = CertificateUtil.getCommonNameFromCertificate(mtlsConfig.getCaCertificate()); - } catch (Exception ex) { - throw new IllegalArgumentException("Certificate is not valid", ex); - } - } - - @Override - public Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - headers.put(CLIENT_ID, this.commonName); - - return next.startCall(call, headers); - } -} diff --git a/server/src/main/java/io/littlehorse/server/auth/RequestAuthorizer.java b/server/src/main/java/io/littlehorse/server/auth/RequestAuthorizer.java index 06a46574e0..dfda3d720c 100644 --- a/server/src/main/java/io/littlehorse/server/auth/RequestAuthorizer.java +++ b/server/src/main/java/io/littlehorse/server/auth/RequestAuthorizer.java @@ -32,7 +32,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class RequestAuthorizer implements ServerAuthorizer { +public class RequestAuthorizer implements LHServerInterceptor { private final CoreStoreProvider coreStoreProvider; diff --git a/server/src/main/java/io/littlehorse/server/auth/RequestSanitizer.java b/server/src/main/java/io/littlehorse/server/auth/RequestSanitizer.java deleted file mode 100644 index bc1c5d903e..0000000000 --- a/server/src/main/java/io/littlehorse/server/auth/RequestSanitizer.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.littlehorse.server.auth; - -import io.grpc.Context; -import io.grpc.Contexts; -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; - -public class RequestSanitizer implements ServerAuthorizer { - - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - Context context = Context.current(); - headers.discardAll(CLIENT_ID); - return Contexts.interceptCall(context, call, headers, next); - } -} diff --git a/server/src/main/java/io/littlehorse/server/auth/authenticators/InsecureAuthenticator.java b/server/src/main/java/io/littlehorse/server/auth/authenticators/InsecureAuthenticator.java new file mode 100644 index 0000000000..2a884ea275 --- /dev/null +++ b/server/src/main/java/io/littlehorse/server/auth/authenticators/InsecureAuthenticator.java @@ -0,0 +1,26 @@ +package io.littlehorse.server.auth.authenticators; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.littlehorse.common.LHConstants; +import io.littlehorse.server.auth.LHServerInterceptor; + +/** + * Authenticator for insecure server listeners. Sets the principal id to `anonymous` for all requests. + */ +public class InsecureAuthenticator implements LHServerInterceptor { + + @Override + public Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + + headers.put(CLIENT_ID, LHConstants.ANONYMOUS_PRINCIPAL); + return next.startCall(call, headers); + } + + public static InsecureAuthenticator create() { + return new InsecureAuthenticator(); + } +} diff --git a/server/src/main/java/io/littlehorse/server/auth/authenticators/MTLSAuthenticator.java b/server/src/main/java/io/littlehorse/server/auth/authenticators/MTLSAuthenticator.java new file mode 100644 index 0000000000..df51dab2e6 --- /dev/null +++ b/server/src/main/java/io/littlehorse/server/auth/authenticators/MTLSAuthenticator.java @@ -0,0 +1,73 @@ +package io.littlehorse.server.auth.authenticators; + +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.Status; +import io.littlehorse.common.LHConstants; +import io.littlehorse.server.auth.LHServerInterceptor; +import java.security.Principal; +import java.util.List; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import lombok.extern.slf4j.Slf4j; + +/** + * Authenticator for requests on MTLS listeners. Sets the principal id to the common name of the client + * certificate. + */ +@Slf4j +public class MTLSAuthenticator implements LHServerInterceptor { + + @Override + public Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + + SSLSession clientTlsInfo = call.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); + + if (clientTlsInfo == null) { + throw new IllegalStateException( + "MTLSAuthenticator should only be used on MTLS ports, so SSLSession should be present"); + } + + String commonName; + try { + // Determine commonName from the client certificate + Principal peerPrincipal = clientTlsInfo.getPeerPrincipal(); // NOT a littlehorse Principal + + LdapName ln = new LdapName(peerPrincipal.getName()); + + List commonNames = ln.getRdns().stream() + .filter(rdn -> rdn.getType().equalsIgnoreCase("CN")) + .map(Rdn::getValue) + .map(Object::toString) + .toList(); + + if (commonNames.size() == 0) { + // This happens when the client certificate does not have a CommonName. + // Note that the interceptor wouldn't even be called if the SSL handshake failed, + // so we know at this point that the client presented a valid certificate. + // + // Since they did not set a commonName on the certificate, we treat them as + // anonymous. + commonName = LHConstants.ANONYMOUS_PRINCIPAL; + } else { + commonName = commonNames.get(0); + } + + headers.put(CLIENT_ID, commonName); + log.trace("Got common name from client certificate: {}", commonName); + return next.startCall(call, headers); + } catch (InvalidNameException | SSLPeerUnverifiedException e) { + // close the call as unauthenticated + log.trace("Closing the call as unauthenticated due to certiciate exception", e); + call.close(Status.UNAUTHENTICATED.withDescription("Invalid certificate"), new Metadata()); + return new ServerCall.Listener<>() {}; + } + } +} diff --git a/server/src/main/java/io/littlehorse/server/auth/OAuthServerAuthenticator.java b/server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthAuthenticator.java similarity index 89% rename from server/src/main/java/io/littlehorse/server/auth/OAuthServerAuthenticator.java rename to server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthAuthenticator.java index c1c231fa02..ac6def30ac 100644 --- a/server/src/main/java/io/littlehorse/server/auth/OAuthServerAuthenticator.java +++ b/server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthAuthenticator.java @@ -1,4 +1,4 @@ -package io.littlehorse.server.auth; +package io.littlehorse.server.auth.authenticators; import com.google.common.base.Strings; import com.google.common.cache.Cache; @@ -10,16 +10,21 @@ import io.grpc.ServerCallHandler; import io.grpc.Status; import io.littlehorse.sdk.common.auth.TokenStatus; +import io.littlehorse.server.auth.LHServerInterceptor; +import io.littlehorse.server.auth.OAuthConfig; +import io.littlehorse.server.auth.UnauthenticatedException; import java.util.Optional; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; /** + * Determines the Principal ID from the OAuth token. + * * Example: * https://github.com/grpc/grpc-java/blob/master/examples/example-oauth/src/main/java/io/grpc/examples/oauth/OAuth2ServerInterceptor.java */ @Slf4j -public class OAuthServerAuthenticator implements ServerAuthorizer { +public class OAuthAuthenticator implements LHServerInterceptor { private static final Metadata.Key AUTHORIZATION_HEADER_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); @@ -31,7 +36,7 @@ public class OAuthServerAuthenticator implements ServerAuthorizer { private final OAuthClient client; - public OAuthServerAuthenticator(OAuthConfig config) { + public OAuthAuthenticator(OAuthConfig config) { this.client = new OAuthClient(config); } diff --git a/server/src/main/java/io/littlehorse/server/auth/OAuthClient.java b/server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthClient.java similarity index 95% rename from server/src/main/java/io/littlehorse/server/auth/OAuthClient.java rename to server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthClient.java index 58cd814a6a..cec2216b24 100644 --- a/server/src/main/java/io/littlehorse/server/auth/OAuthClient.java +++ b/server/src/main/java/io/littlehorse/server/auth/authenticators/OAuthClient.java @@ -1,4 +1,4 @@ -package io.littlehorse.server.auth; +package io.littlehorse.server.auth.authenticators; import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.Scope; @@ -13,6 +13,8 @@ import com.nimbusds.openid.connect.sdk.OIDCScopeValue; import io.littlehorse.sdk.common.auth.TokenStatus; import io.littlehorse.sdk.common.exception.EntityProviderException; +import io.littlehorse.server.auth.OAuthConfig; +import io.littlehorse.server.auth.UnauthenticatedException; import java.io.IOException; import java.time.Instant; import lombok.extern.slf4j.Slf4j; diff --git a/server/src/main/java/io/littlehorse/server/auth/InternalAuthorizer.java b/server/src/main/java/io/littlehorse/server/auth/internalport/InternalAuthorizer.java similarity index 82% rename from server/src/main/java/io/littlehorse/server/auth/InternalAuthorizer.java rename to server/src/main/java/io/littlehorse/server/auth/internalport/InternalAuthorizer.java index 93257fe3dd..7036765494 100644 --- a/server/src/main/java/io/littlehorse/server/auth/InternalAuthorizer.java +++ b/server/src/main/java/io/littlehorse/server/auth/internalport/InternalAuthorizer.java @@ -1,4 +1,4 @@ -package io.littlehorse.server.auth; +package io.littlehorse.server.auth.internalport; import io.grpc.Context; import io.grpc.Contexts; @@ -9,12 +9,20 @@ import io.littlehorse.common.model.getable.ObjectIdModel; import io.littlehorse.common.model.getable.objectId.PrincipalIdModel; import io.littlehorse.common.model.getable.objectId.TenantIdModel; +import io.littlehorse.server.auth.LHServerInterceptor; import io.littlehorse.server.streams.topology.core.CoreStoreProvider; import io.littlehorse.server.streams.topology.core.RequestExecutionContext; import io.littlehorse.server.streams.util.MetadataCache; import java.util.Objects; -public class InternalAuthorizer implements ServerAuthorizer { +/** + * ServerInterceptor to populate the RequestExecutionContext with the TenantId and PrincipalId + * for interactive queries between LH Servers. + * + * While the request is technically made from one LH Server to another, we want the receiving + * server to treat it as if it were made by the original client. + */ +public class InternalAuthorizer implements LHServerInterceptor { private final Context.Key executionContextKey; private final CoreStoreProvider coreStoreProvider; diff --git a/server/src/main/java/io/littlehorse/server/auth/InternalCallCredentials.java b/server/src/main/java/io/littlehorse/server/auth/internalport/InternalCallCredentials.java similarity index 85% rename from server/src/main/java/io/littlehorse/server/auth/InternalCallCredentials.java rename to server/src/main/java/io/littlehorse/server/auth/internalport/InternalCallCredentials.java index 941a8e85a0..fb118366a9 100644 --- a/server/src/main/java/io/littlehorse/server/auth/InternalCallCredentials.java +++ b/server/src/main/java/io/littlehorse/server/auth/internalport/InternalCallCredentials.java @@ -1,14 +1,20 @@ -package io.littlehorse.server.auth; +package io.littlehorse.server.auth.internalport; import io.grpc.CallCredentials; import io.grpc.Metadata; import io.grpc.Status; import io.littlehorse.common.AuthorizationContext; +import io.littlehorse.server.auth.RequestAuthorizer; import io.littlehorse.server.streams.topology.core.BackgroundContext; import io.littlehorse.server.streams.topology.core.ProcessorExecutionContext; import io.littlehorse.server.streams.topology.core.RequestExecutionContext; import java.util.concurrent.Executor; +/** + * Call Credentials that allows the LH Server to propagate information about the calling Tenant + * and Principal when making an Interactive Query between two different LH Servers on the + * internal port. + */ public class InternalCallCredentials extends CallCredentials { private final AuthorizationContext currentAuthorization; diff --git a/server/src/main/java/io/littlehorse/server/listener/ServerListenerConfig.java b/server/src/main/java/io/littlehorse/server/listener/ServerListenerConfig.java index d1aa0bf25c..2279bd8183 100644 --- a/server/src/main/java/io/littlehorse/server/listener/ServerListenerConfig.java +++ b/server/src/main/java/io/littlehorse/server/listener/ServerListenerConfig.java @@ -6,10 +6,10 @@ import io.grpc.TlsServerCredentials; import io.littlehorse.common.LHServerConfig; import io.littlehorse.server.auth.AuthorizationProtocol; -import io.littlehorse.server.auth.InsecureServerAuthorizer; -import io.littlehorse.server.auth.MTLSServerAuthorizer; -import io.littlehorse.server.auth.OAuthServerAuthenticator; -import io.littlehorse.server.auth.ServerAuthorizer; +import io.littlehorse.server.auth.LHServerInterceptor; +import io.littlehorse.server.auth.authenticators.InsecureAuthenticator; +import io.littlehorse.server.auth.authenticators.MTLSAuthenticator; +import io.littlehorse.server.auth.authenticators.OAuthAuthenticator; import java.io.IOException; import lombok.Builder; import lombok.Getter; @@ -67,11 +67,11 @@ public ServerCredentials getCredentials() { } } - public ServerAuthorizer getServerAuthorizer() { + public LHServerInterceptor getRequestAuthenticator() { return switch (authorizationProtocol) { - case OAUTH -> new OAuthServerAuthenticator(config.getOAuthConfig()); - case MTLS -> new MTLSServerAuthorizer(config.getMTLSConfiguration(name)); - default -> InsecureServerAuthorizer.create(); + case OAUTH -> new OAuthAuthenticator(config.getOAuthConfig()); + case MTLS -> new MTLSAuthenticator(); + default -> InsecureAuthenticator.create(); }; } } diff --git a/server/src/main/java/io/littlehorse/server/streams/BackendInternalComms.java b/server/src/main/java/io/littlehorse/server/streams/BackendInternalComms.java index 0750023984..402d58989d 100644 --- a/server/src/main/java/io/littlehorse/server/streams/BackendInternalComms.java +++ b/server/src/main/java/io/littlehorse/server/streams/BackendInternalComms.java @@ -53,8 +53,8 @@ import io.littlehorse.sdk.common.proto.LHHostInfo; import io.littlehorse.sdk.common.proto.WorkflowEvent; import io.littlehorse.server.GlobalExceptionHandler; -import io.littlehorse.server.auth.InternalAuthorizer; -import io.littlehorse.server.auth.InternalCallCredentials; +import io.littlehorse.server.auth.internalport.InternalAuthorizer; +import io.littlehorse.server.auth.internalport.InternalCallCredentials; import io.littlehorse.server.listener.AdvertisedListenerConfig; import io.littlehorse.server.streams.lhinternalscan.InternalScan; import io.littlehorse.server.streams.lhinternalscan.publicrequests.scanfilter.ScanFilterModel; diff --git a/server/src/main/java/io/littlehorse/server/streams/topology/core/ProcessorExecutionContext.java b/server/src/main/java/io/littlehorse/server/streams/topology/core/ProcessorExecutionContext.java index 07a540b804..07b283a73f 100644 --- a/server/src/main/java/io/littlehorse/server/streams/topology/core/ProcessorExecutionContext.java +++ b/server/src/main/java/io/littlehorse/server/streams/topology/core/ProcessorExecutionContext.java @@ -12,7 +12,7 @@ import io.littlehorse.common.proto.Command; import io.littlehorse.sdk.common.proto.LHHostInfo; import io.littlehorse.server.LHServer; -import io.littlehorse.server.auth.InternalCallCredentials; +import io.littlehorse.server.auth.internalport.InternalCallCredentials; import io.littlehorse.server.streams.ServerTopology; import io.littlehorse.server.streams.storeinternals.GetableManager; import io.littlehorse.server.streams.storeinternals.ReadOnlyMetadataManager; diff --git a/server/src/test/java/io/littlehorse/server/auth/RequestSanitizerTest.java b/server/src/test/java/io/littlehorse/server/auth/InsecureAuthenticatorTest.java similarity index 66% rename from server/src/test/java/io/littlehorse/server/auth/RequestSanitizerTest.java rename to server/src/test/java/io/littlehorse/server/auth/InsecureAuthenticatorTest.java index a830ac0b51..3c021628a7 100644 --- a/server/src/test/java/io/littlehorse/server/auth/RequestSanitizerTest.java +++ b/server/src/test/java/io/littlehorse/server/auth/InsecureAuthenticatorTest.java @@ -12,15 +12,17 @@ import io.grpc.ServerInterceptors; import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; +import io.littlehorse.common.LHConstants; import io.littlehorse.sdk.common.proto.LittleHorseGrpc; +import io.littlehorse.server.auth.authenticators.InsecureAuthenticator; import java.util.Collection; import lombok.Getter; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -class RequestSanitizerTest { +class InsecureAuthenticatorTest { - private RequestSanitizer sanitizer = new RequestSanitizer(); + private InsecureAuthenticator authenticator = new InsecureAuthenticator(); private TrackableInterceptor trackableInterceptor = new TrackableInterceptor(); private final ServerServiceDefinition testServiceDefinition = buildTestServiceDefinition( ServerServiceDefinition.builder(LittleHorseGrpc.getServiceDescriptor()), @@ -30,19 +32,30 @@ class RequestSanitizerTest { private final Metadata requestHeaders = new Metadata(); @Test - public void shouldRemoveInternalHeader() { - requestHeaders.put(ServerAuthorizer.CLIENT_ID, "root"); - requestHeaders.put(ServerAuthorizer.TENANT_ID, "my-tenant"); + public void shouldSetPrincipalToAnonymousWhenProvidedByUser() { + requestHeaders.put(LHServerInterceptor.CLIENT_ID, "root"); + requestHeaders.put(LHServerInterceptor.TENANT_ID, "my-tenant"); startCall(); Metadata resolvedHeaders = trackableInterceptor.getHeaders(); Assertions.assertThat(resolvedHeaders).isNotNull(); - Assertions.assertThat(resolvedHeaders.get(ServerAuthorizer.CLIENT_ID)).isNull(); - Assertions.assertThat(resolvedHeaders.get(ServerAuthorizer.TENANT_ID)).isEqualTo("my-tenant"); + Assertions.assertThat(resolvedHeaders.get(LHServerInterceptor.CLIENT_ID)) + .isEqualTo(LHConstants.ANONYMOUS_PRINCIPAL); + Assertions.assertThat(resolvedHeaders.get(LHServerInterceptor.TENANT_ID)) + .isEqualTo("my-tenant"); + } + + @Test + public void shouldSetPrincipalToAnonymous() { + startCall(); + Metadata resolvedHeaders = trackableInterceptor.getHeaders(); + Assertions.assertThat(resolvedHeaders).isNotNull(); + Assertions.assertThat(resolvedHeaders.get(LHServerInterceptor.CLIENT_ID)) + .isEqualTo(LHConstants.ANONYMOUS_PRINCIPAL); } private void startCall() { ServerServiceDefinition intercept = - ServerInterceptors.intercept(testServiceDefinition, sanitizer, trackableInterceptor); + ServerInterceptors.intercept(testServiceDefinition, authenticator, trackableInterceptor); @SuppressWarnings("unchecked") ServerMethodDefinition def = (ServerMethodDefinition) Iterables.get(intercept.getMethods(), 0); @@ -58,7 +71,7 @@ private ServerServiceDefinition buildTestServiceDefinition( } @Getter - private static class TrackableInterceptor implements ServerAuthorizer { + private static class TrackableInterceptor implements LHServerInterceptor { private Metadata headers; @Override diff --git a/server/src/test/java/io/littlehorse/server/auth/RequestAuthorizerTest.java b/server/src/test/java/io/littlehorse/server/auth/RequestAuthorizerTest.java index 5676fc8081..74d7efd046 100644 --- a/server/src/test/java/io/littlehorse/server/auth/RequestAuthorizerTest.java +++ b/server/src/test/java/io/littlehorse/server/auth/RequestAuthorizerTest.java @@ -89,7 +89,7 @@ private void startCall() { @Test public void supportAnonymousPrincipalForDefaultTenant() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn(null); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn(null); startCall(); assertThat(resolvedAuthContext).isNotNull(); @@ -102,7 +102,7 @@ public void supportAnonymousPrincipalForDefaultTenant() { @Test public void supportAnonymousPrincipalWhenClientIdIsNotFound() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("principal-id"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("principal-id"); startCall(); assertThat(resolvedAuthContext.principalId().getId()).isEqualTo(LHConstants.ANONYMOUS_PRINCIPAL); assertThat(resolvedAuthContext.acls()) @@ -111,8 +111,8 @@ public void supportAnonymousPrincipalWhenClientIdIsNotFound() { @Test public void supportPrincipalForSpecificTenant() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("principal-id"); - when(mockMetadata.get(ServerAuthorizer.TENANT_ID)).thenReturn("my-tenant"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("principal-id"); + when(mockMetadata.get(LHServerInterceptor.TENANT_ID)).thenReturn("my-tenant"); PrincipalModel newPrincipal = new PrincipalModel(); newPrincipal.setId(new PrincipalIdModel("principal-id")); newPrincipal.setGlobalAcls(TestUtil.singleAdminAcl("name")); @@ -126,8 +126,8 @@ public void supportPrincipalForSpecificTenant() { @Test public void supportPermissionDeniedForNonExistingTenants() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("principal-id"); - when(mockMetadata.get(ServerAuthorizer.TENANT_ID)).thenReturn("my-missing-tenant"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("principal-id"); + when(mockMetadata.get(LHServerInterceptor.TENANT_ID)).thenReturn("my-missing-tenant"); PrincipalModel newPrincipal = new PrincipalModel(); newPrincipal.setId(new PrincipalIdModel("principal-id")); newPrincipal.setGlobalAcls(TestUtil.singleAdminAcl("name")); @@ -138,7 +138,7 @@ public void supportPermissionDeniedForNonExistingTenants() { @Test public void supportAnonymousPrincipalWhenPrincipalIdIsNotFound() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("principal-id"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("principal-id"); metadataManager.put(new TenantModel("my-tenant")); startCall(); assertThat(resolvedAuthContext.principalId().getId()).isEqualTo(LHConstants.ANONYMOUS_PRINCIPAL); @@ -164,7 +164,7 @@ public void setup() { @Test public void supportRequestAuthorizationForAdminPrincipals() { - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("admin-principal"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("admin-principal"); startCall(); assertThat(resolvedAuthContext).isNotNull(); } @@ -174,7 +174,7 @@ public void supportRequiredAclValidation() { MethodDescriptor mockMethod = mock(); when(mockCall.getMethodDescriptor()).thenReturn(mockMethod); when(mockMethod.getBareMethodName()).thenReturn("PutTaskDef"); - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("limited-principal"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("limited-principal"); startCall(); Mockito.verify(mockCall).close(any(), eq(mockMetadata)); } @@ -184,7 +184,7 @@ public void shouldRejectTenantResourceRequestFromTenantAdminPrincipal() { MethodDescriptor mockMethod = mock(); when(mockCall.getMethodDescriptor()).thenReturn(mockMethod); when(mockMethod.getBareMethodName()).thenReturn("PutTenant"); - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("tenant-admin-principal"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("tenant-admin-principal"); startCall(); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); Mockito.verify(mockCall).close(statusCaptor.capture(), eq(mockMetadata)); @@ -199,7 +199,7 @@ public void shouldRejectPrincipalResourceRequestFromTenantAdminPrincipal() { MethodDescriptor mockMethod = mock(); when(mockCall.getMethodDescriptor()).thenReturn(mockMethod); when(mockMethod.getBareMethodName()).thenReturn("PutPrincipal"); - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("tenant-admin-principal"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("tenant-admin-principal"); startCall(); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); Mockito.verify(mockCall).close(statusCaptor.capture(), eq(mockMetadata)); @@ -214,8 +214,8 @@ public void supportTenantAdmins() { MethodDescriptor mockMethod = mock(); when(mockCall.getMethodDescriptor()).thenReturn(mockMethod); when(mockMethod.getBareMethodName()).thenReturn("PutTaskDef"); - when(mockMetadata.get(ServerAuthorizer.CLIENT_ID)).thenReturn("tenant-admin-principal"); - when(mockMetadata.get(ServerAuthorizer.TENANT_ID)).thenReturn("my-tenant"); + when(mockMetadata.get(LHServerInterceptor.CLIENT_ID)).thenReturn("tenant-admin-principal"); + when(mockMetadata.get(LHServerInterceptor.TENANT_ID)).thenReturn("my-tenant"); startCall(); assertThat(resolvedAuthContext).isNotNull(); } @@ -261,7 +261,7 @@ public MethodDescriptor getMethodDescriptor() { } }; final Metadata mockMetadata = new Metadata(); - mockMetadata.put(ServerAuthorizer.CLIENT_ID, principalId); + mockMetadata.put(LHServerInterceptor.CLIENT_ID, principalId); PrincipalModel newPrincipal = new PrincipalModel(); newPrincipal.setId(new PrincipalIdModel(principalId)); newPrincipal.setGlobalAcls(TestUtil.singleAdminAcl("name")); @@ -313,7 +313,7 @@ private ServerServiceDefinition buildContextPropagationVerifierServiceDefinition definitionBuilder = definitionBuilder.addMethod(method, (call, headers) -> { String principalId = contextKey.get().authorization().principalId().toString(); - assertThat(principalId).isEqualTo(headers.get(ServerAuthorizer.CLIENT_ID)); + assertThat(principalId).isEqualTo(headers.get(LHServerInterceptor.CLIENT_ID)); return new NoopServerCall.NoopServerCallListener<>(); }); } diff --git a/test-utils/src/main/java/io/littlehorse/test/internal/TestBootstrapper.java b/test-utils/src/main/java/io/littlehorse/test/internal/TestBootstrapper.java index f89c0a9672..6416ca5ddc 100644 --- a/test-utils/src/main/java/io/littlehorse/test/internal/TestBootstrapper.java +++ b/test-utils/src/main/java/io/littlehorse/test/internal/TestBootstrapper.java @@ -6,7 +6,7 @@ import io.littlehorse.common.model.getable.objectId.TenantIdModel; import io.littlehorse.sdk.common.config.LHConfig; import io.littlehorse.sdk.common.proto.LittleHorseGrpc.LittleHorseBlockingStub; -import io.littlehorse.server.auth.ServerAuthorizer; +import io.littlehorse.server.auth.LHServerInterceptor; import java.util.concurrent.Executor; public interface TestBootstrapper { @@ -28,7 +28,7 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, Met try { Metadata headers = new Metadata(); if (tenantId != null && tenantId.getId() != null) { - headers.put(ServerAuthorizer.TENANT_ID, tenantId.getId()); + headers.put(LHServerInterceptor.TENANT_ID, tenantId.getId()); metadataApplier.apply(headers); } } catch (Exception e) { From 2ba4808d40f17d3b8bfb728ec2586b5c55814e52 Mon Sep 17 00:00:00 2001 From: Hazim Arafa Date: Mon, 25 Nov 2024 11:21:19 -0800 Subject: [PATCH 2/2] refactor(dashboard): optimize components --- .../(diagram)/components/Forms/WfRunForm.tsx | 25 ++++--- .../Forms/components/BaseFormField.tsx | 75 +++++++++++++++++++ .../components/Forms/components/FormInput.tsx | 68 +++++------------ .../Forms/components/FormSelect.tsx | 58 +++++--------- .../Forms/components/FormTextarea.tsx | 46 ++++-------- .../components/Modals/ExecuteWorkflowRun.tsx | 11 ++- .../wfRun/[...ids]/components/Variables.tsx | 1 + 7 files changed, 152 insertions(+), 132 deletions(-) create mode 100644 dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/BaseFormField.tsx diff --git a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/WfRunForm.tsx b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/WfRunForm.tsx index b4663c7311..9a76c8bb27 100644 --- a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/WfRunForm.tsx +++ b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/WfRunForm.tsx @@ -23,21 +23,24 @@ export const WfRunForm = forwardRef(({ wfSpecVariables, o onSubmit(data) } + // Sort variables so required fields come first + const sortedVariables = wfSpecVariables.sort((a, b) => { + if (a.required === b.required) return 0 + return a.required ? -1 : 1 + }) + return ( -
+
- - + +
- {!!wfSpecVariables?.length && - wfSpecVariables.map((variable: ThreadVarDef) => ( + {!!sortedVariables.length && + sortedVariables.map((variable: ThreadVarDef) => ( ))} diff --git a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/BaseFormField.tsx b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/BaseFormField.tsx new file mode 100644 index 0000000000..c45ed6e3eb --- /dev/null +++ b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/BaseFormField.tsx @@ -0,0 +1,75 @@ +import { Label } from '@/components/ui/label' +import { FormFieldProp } from '@/types' +import { CircleAlert } from 'lucide-react' +import { FC, ReactNode } from 'react' +import { useFormContext } from 'react-hook-form' +import { accessLevels } from '../../../wfSpec/[...props]/components/Variables' +import { Button } from '@/components/ui/button' + +type BaseFormFieldProps = FormFieldProp & { + children: ReactNode + isDisabled: boolean + setIsDisabled: (disabled: boolean) => void +} + +export const BaseFormField: FC = ({ + variables, + formState: { errors }, + children, + isDisabled, + setIsDisabled +}) => { + const { setValue, trigger } = useFormContext() + + if (!variables?.varDef?.name) return null + + const { + varDef: { name }, + required, + accessLevel, + } = variables + + const handleSetNull = () => { + setValue(name, null) + trigger(name) + setIsDisabled(true) + } + + const handleEnable = () => { + setIsDisabled(false) + } + + return ( +
+
+ + {!required && ( + + )} +
+ {children} + {errors[name] && ( +

+ + {errors[name]?.message} +

+ )} +
+ ) +} diff --git a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormInput.tsx b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormInput.tsx index e2ba9a1783..804552b103 100644 --- a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormInput.tsx +++ b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormInput.tsx @@ -1,77 +1,49 @@ import { VARIABLE_TYPES } from '@/app/constants' import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +import { cn } from '@/components/utils' import { FormFieldProp } from '@/types' import { VariableType } from 'littlehorse-client/proto' -import { CircleAlert } from 'lucide-react' -import { FC, useState } from 'react' -import { accessLevels } from '../../../wfSpec/[...props]/components/Variables' +import { FC, useEffect, useState } from 'react' +import { useFormContext } from 'react-hook-form' +import { BaseFormField } from './BaseFormField' export const FormInput: FC = props => { - const [isDisabled, setIsDisabled] = useState(false) + const [isDisabled, setIsDisabled] = useState(!props.variables?.required) + const { setValue } = useFormContext() + + useEffect(() => { + if (!props.variables?.required && props.variables?.varDef?.name) { + setValue(props.variables.varDef.name, null) + } + }, [props.variables, setValue]) + if (!props.variables?.varDef?.name) return null const { variables: { varDef: { type, name }, required, - accessLevel, }, register, formState: { errors }, } = props - const variableToType = (variable: VariableType) => { - switch (variable) { - case VariableType.INT: - return 'number' - case VariableType.DOUBLE: - return 'number' - case VariableType.BYTES: - return 'number' - case VariableType.STR: - return 'text' - default: - return 'text' - } - } - - const setValue = (value: number | string) => { - if (value === null) return value - return variableToType(type) === 'number' ? parseFloat(value?.toString()) || undefined : value || undefined - } - return ( -
-
- -
+ { + return type === VariableType.INT || type === VariableType.DOUBLE ? parseFloat(value) || undefined : value + }, })} /> - {errors[name] && ( -

- - {errors[name]?.message} -

- )} -
+ ) } diff --git a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormSelect.tsx b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormSelect.tsx index c3ba897fae..809e9143c4 100644 --- a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormSelect.tsx +++ b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormSelect.tsx @@ -1,20 +1,19 @@ -import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { FormFieldProp } from '@/types' -import { CircleAlert } from 'lucide-react' -import { FC, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' -import { accessLevels } from '../../../wfSpec/[...props]/components/Variables' +import { BaseFormField } from './BaseFormField' +import { cn } from '@/components/utils' export const FormSelect: FC = props => { - const { - register, - setValue, - formState: { errors }, - getValues, - trigger, - } = useFormContext() - const [isDisabled, setIsDisabled] = useState(false) + const [isDisabled, setIsDisabled] = useState(!props.variables?.required) + const { register, setValue, getValues, trigger } = useFormContext() + + useEffect(() => { + if (!props.variables?.required && props.variables?.varDef?.name) { + setValue(props.variables.varDef.name, null) + } + }, [props.variables, setValue]) if (!props.variables?.varDef?.name) return null @@ -22,55 +21,36 @@ export const FormSelect: FC = props => { variables: { varDef: { name }, required, - accessLevel, }, } = props const handleChange = (value: string) => { - if (value === 'none') setValue(name, undefined) - else { - const booleanValue = value === 'true' - setValue(name, booleanValue) - } + const booleanValue = value === 'true' + setValue(name, booleanValue) trigger(name) } const value = getValues(name) return ( -
-
- -
+ - {errors[name]?.message && ( -

- - {String(errors[name]?.message)} -

- )} -
+ ) } diff --git a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormTextarea.tsx b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormTextarea.tsx index adebb0dce4..336def7bb1 100644 --- a/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormTextarea.tsx +++ b/dashboard/src/app/(authenticated)/(diagram)/components/Forms/components/FormTextarea.tsx @@ -1,23 +1,26 @@ -import { Label } from '@/components/ui/label' +import { VARIABLE_TYPES } from '@/app/constants' import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/components/utils' import { FormFieldProp } from '@/types' -import { FC, useState } from 'react' - -import { VARIABLE_TYPES } from '@/app/constants' -import { CircleAlert } from 'lucide-react' +import { FC, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' -import { accessLevels } from '../../../wfSpec/[...props]/components/Variables' +import { BaseFormField } from './BaseFormField' import { getValidation } from './validation' export const FormTextarea: FC = props => { - const [isDisabled, setIsDisabled] = useState(false) + const [isDisabled, setIsDisabled] = useState(!props.variables?.required) const { setValue, trigger } = useFormContext() - if (!props.variables?.varDef?.name) return + useEffect(() => { + if (!props.variables?.required && props.variables?.varDef?.name) { + setValue(props.variables.varDef.name, null) + } + }, [props.variables, setValue]) + + if (!props.variables?.varDef?.name) return null const { variables: { varDef: { type, name }, - accessLevel, required, }, register, @@ -25,20 +28,9 @@ export const FormTextarea: FC = props => { } = props return ( -
-
- -
+