diff options
Diffstat (limited to 'athenz-identity-provider-service/src/main/java')
5 files changed, 26 insertions, 169 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java index 89e7e340641..f1a93e58526 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java @@ -14,7 +14,6 @@ import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -35,15 +34,12 @@ import java.util.stream.Stream; */ public class InstanceValidator { + private static final AthenzService TENANT_DOCKER_CONTAINER_IDENTITY = new AthenzService("vespa.vespa.tenant"); private static final Logger log = Logger.getLogger(InstanceValidator.class.getName()); static final String SERVICE_PROPERTIES_DOMAIN_KEY = "identity.domain"; static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service"; static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz."; - public static final String SAN_IPS_ATTRNAME = "sanIP"; - public static final String SAN_DNS_ATTRNAME = "sanDNS"; - - private final AthenzService tenantDockerContainerIdentity; private final IdentityDocumentSigner signer; private final KeyProvider keyProvider; private final SuperModelProvider superModelProvider; @@ -52,21 +48,18 @@ public class InstanceValidator { @Inject public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider, - NodeRepository nodeRepository, - AthenzProviderServiceConfig config) { - this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner(), new AthenzService(config.tenantService())); + NodeRepository nodeRepository) { + this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner()); } public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider, NodeRepository nodeRepository, - IdentityDocumentSigner identityDocumentSigner, - AthenzService tenantIdentity){ + IdentityDocumentSigner identityDocumentSigner){ this.keyProvider = keyProvider; this.superModelProvider = superModelProvider; this.nodeRepository = nodeRepository; this.signer = identityDocumentSigner; - this.tenantDockerContainerIdentity = tenantIdentity; } public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { @@ -103,7 +96,7 @@ public class InstanceValidator { log.log(LogLevel.INFO, () -> String.format("Accepting refresh for instance with identity '%s', provider '%s', instanceId '%s'.", new AthenzService(confirmation.domain, confirmation.service).getFullName(), confirmation.provider, - confirmation.attributes.get(SAN_DNS_ATTRNAME))); + confirmation.attributes.get("sanDNS"))); try { return validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); } catch (Exception e) { @@ -114,7 +107,7 @@ public class InstanceValidator { private VespaUniqueInstanceId getVespaUniqueInstanceId(InstanceConfirmation instanceConfirmation) { // Find a list of SAN DNS - List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME)) + List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get("sanDNS")) .map(s -> s.split(",")) .map(Arrays::asList) .map(List::stream) @@ -131,7 +124,7 @@ public class InstanceValidator { private boolean validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) { if(vespaUniqueInstanceId == null) { - log.log(LogLevel.WARNING, "Unable to find unique instance ID in refresh request: " + confirmation.toString()); + log.log(LogLevel.WARNING, "Unabe to find unique instance ID in refresh request: " + confirmation.toString()); return false; } @@ -147,7 +140,7 @@ public class InstanceValidator { } // Find list of ipaddresses - List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get(SAN_IPS_ATTRNAME)) + List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get("sanIP")) .map(s -> s.split(",")) .map(Arrays::asList) .map(List::stream) @@ -191,7 +184,7 @@ public class InstanceValidator { return false; } - if (tenantDockerContainerIdentity.equals(new AthenzService(domain, service))) { + if (TENANT_DOCKER_CONTAINER_IDENTITY.equals(new AthenzService(domain, service))) { return true; } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java index b5a2405167a..308127e29c7 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java @@ -4,17 +4,14 @@ package com.yahoo.vespa.hosted.ca; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.SubjectAlternativeName; import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.security.X509CertificateUtils; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; -import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; @@ -56,22 +53,14 @@ public class Certificates { /** Returns instance ID parsed from the Subject Alternative Names in given csr */ public static String instanceIdFrom(Pkcs10Csr csr) { - return getInstanceIdFromSAN(csr.getSubjectAlternativeNames()) - .orElseThrow(() -> new IllegalArgumentException("No instance ID found in CSR")); - } - - public static Optional<String> instanceIdFrom(X509Certificate certificate) { - return getInstanceIdFromSAN(X509CertificateUtils.getSubjectAlternativeNames(certificate)); - } - - private static Optional<String> getInstanceIdFromSAN(List<SubjectAlternativeName> subjectAlternativeNames) { - return subjectAlternativeNames.stream() - .filter(san -> san.getType() == DNS_NAME) - .map(SubjectAlternativeName::getValue) - .map(Certificates::parseInstanceId) - .flatMap(Optional::stream) - .map(VespaUniqueInstanceId::asDottedString) - .findFirst(); + return csr.getSubjectAlternativeNames().stream() + .filter(san -> san.getType() == DNS_NAME) + .map(SubjectAlternativeName::getValue) + .map(Certificates::parseInstanceId) + .flatMap(Optional::stream) + .map(VespaUniqueInstanceId::asDottedString) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No instance ID found in CSR")); } private static Optional<VespaUniqueInstanceId> parseInstanceId(String dnsName) { @@ -85,11 +74,4 @@ public class Certificates { } } - public static String getSubjectAlternativeNames(Pkcs10Csr csr, SubjectAlternativeName.Type sanType) { - return csr.getSubjectAlternativeNames().stream() - .map(SubjectAlternativeName::decode) - .filter(san -> san.getType() == sanType) - .map(SubjectAlternativeName::getValue) - .collect(Collectors.joining(",")); - } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java index 564848fd5c8..2a2b702d21b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java @@ -2,7 +2,6 @@ package com.yahoo.vespa.hosted.ca.instance; import com.yahoo.security.Pkcs10Csr; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import java.util.Objects; @@ -17,10 +16,10 @@ public class InstanceRegistration { private final String provider; private final String domain; private final String service; - private final SignedIdentityDocument attestationData; + private final String attestationData; private final Pkcs10Csr csr; - public InstanceRegistration(String provider, String domain, String service, SignedIdentityDocument attestationData, Pkcs10Csr csr) { + public InstanceRegistration(String provider, String domain, String service, String attestationData, Pkcs10Csr csr) { this.provider = Objects.requireNonNull(provider, "provider must be non-null"); this.domain = Objects.requireNonNull(domain, "domain must be non-null"); this.service = Objects.requireNonNull(service, "service must be non-null"); @@ -44,7 +43,7 @@ public class InstanceRegistration { } /** Host document describing this instance (received from config server) */ - public SignedIdentityDocument attestationData() { + public String attestationData() { return attestationData; } @@ -76,8 +75,8 @@ public class InstanceRegistration { "provider='" + provider + '\'' + ", domain='" + domain + '\'' + ", service='" + service + '\'' + - ", attestationData='" + attestationData.toString() + '\'' + - ", csr=" + csr.toString() + + ", attestationData='" + attestationData + '\'' + + ", csr=" + csr + '}'; } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java index 4c01b0943e4..1b9bdcdb987 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java @@ -6,24 +6,16 @@ import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.container.jdisc.secretstore.SecretStore; -import com.yahoo.jdisc.http.servlet.ServletRequest; -import com.yahoo.log.LogLevel; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.Path; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.security.KeyUtils; -import com.yahoo.security.SubjectAlternativeName; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Slime; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceConfirmation; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator; import com.yahoo.vespa.hosted.ca.Certificates; import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; -import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh; import com.yahoo.yolean.Exceptions; import java.io.IOException; @@ -31,10 +23,6 @@ import java.io.UncheckedIOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Clock; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; @@ -55,20 +43,18 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private final Certificates certificates; private final String caPrivateKeySecretName; private final String caCertificateSecretName; - private final InstanceValidator instanceValidator; @Inject - public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) { - this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig, instanceValidator); + public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig) { + this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig); } - CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) { + CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig) { super(ctx); this.secretStore = secretStore; this.certificates = certificates; this.caPrivateKeySecretName = athenzProviderServiceConfig.secretName(); this.caCertificateSecretName = athenzProviderServiceConfig.domain() + ".ca.cert"; - this.instanceValidator = instanceValidator; } @Override @@ -95,14 +81,6 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private HttpResponse registerInstance(HttpRequest request) { var instanceRegistration = deserializeRequest(request, InstanceSerializer::registrationFromSlime); - - InstanceConfirmation confirmation = new InstanceConfirmation(instanceRegistration.provider(), instanceRegistration.domain(), instanceRegistration.service(), EntityBindingsMapper.toSignedIdentityDocumentEntity(instanceRegistration.attestationData())); - confirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); - confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.DNS_NAME)); - if (!instanceValidator.isValidInstance(confirmation)) { - log.log(LogLevel.INFO, "Invalid instance registration for " + instanceRegistration.toString()); - return ErrorResponse.forbidden("Unable to launch service: " +instanceRegistration.service()); - } var certificate = certificates.create(instanceRegistration.csr(), caCertificate(), caPrivateKey()); var instanceId = Certificates.instanceIdFrom(instanceRegistration.csr()); var identity = new InstanceIdentity(instanceRegistration.provider(), instanceRegistration.service(), instanceId, @@ -113,65 +91,21 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) { var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime); var instanceIdFromCsr = Certificates.instanceIdFrom(instanceRefresh.csr()); - var athenzService = new AthenzService(request.getJDiscRequest().getUserPrincipal().getName()); if (!instanceIdFromCsr.equals(instanceId)) { throw new IllegalArgumentException("Mismatch between instance ID in URL path and instance ID in CSR " + "[instanceId=" + instanceId + ",instanceIdFromCsr=" + instanceIdFromCsr + "]"); } - - // Verify that the csr instance id matches one of the certificates in the chain - refreshesSameInstanceId(instanceIdFromCsr, request); - - - // Validate that there is no privilege escalation (can only refresh same service) - refreshesSameService(instanceRefresh, athenzService); - - InstanceConfirmation instanceConfirmation = new InstanceConfirmation(provider, athenzService.getDomain().getName(), athenzService.getName(), null); - instanceConfirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); - instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.DNS_NAME)); - if(!instanceValidator.isValidRefresh(instanceConfirmation)) { - return ErrorResponse.forbidden("Unable to refresh cert: " + instanceRefresh.csr().getSubject().toString()); - } - var certificate = certificates.create(instanceRefresh.csr(), caCertificate(), caPrivateKey()); var identity = new InstanceIdentity(provider, service, instanceIdFromCsr, Optional.of(certificate)); return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity)); } - public void refreshesSameInstanceId(String csrInstanceId, HttpRequest request) { - String certificateInstanceId = getRequestCertificateChain(request).stream() - .map(Certificates::instanceIdFrom) - .filter(Optional::isPresent) - .map(Optional::get) - .findAny().orElseThrow(() -> new IllegalArgumentException("No client certificate with instance id in request.")); - - if(! Objects.equals(certificateInstanceId, csrInstanceId)) { - throw new IllegalArgumentException("Mismatch between instance ID in client certificate and instance ID in CSR " + - "[instanceId=" + certificateInstanceId + ",instanceIdFromCsr=" + csrInstanceId + - "]"); - } - } - - private void refreshesSameService(InstanceRefresh instanceRefresh, AthenzService athenzService) { - List<String> commonNames = X509CertificateUtils.getCommonNames(instanceRefresh.csr().getSubject()); - if(commonNames.size() != 1 && !Objects.equals(commonNames.get(0), athenzService.getFullName())) { - throw new IllegalArgumentException(String.format("Invalid request, trying to refresh service %s using service %s.", instanceRefresh.csr().getSubject().getName(), athenzService.getFullName())); - } - } - /** Returns CA certificate from secret store */ private X509Certificate caCertificate() { return X509CertificateUtils.fromPem(secretStore.getSecret(caCertificateSecretName)); } - private List<X509Certificate> getRequestCertificateChain(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().context().get(ServletRequest.JDISC_REQUEST_X509CERT)) - .map(X509Certificate[].class::cast) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - } - /** Returns CA private key from secret store */ private PrivateKey caPrivateKey() { return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(caPrivateKeySecretName)); diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java index c989f85b167..a2537cd68f1 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java @@ -3,25 +3,12 @@ package com.yahoo.vespa.hosted.ca.restapi; import com.yahoo.security.Pkcs10CsrUtils; import com.yahoo.security.X509CertificateUtils; -import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; -import com.yahoo.text.StringUtilities; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh; import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration; -import java.math.BigDecimal; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.HashSet; -import java.util.Set; - /** * @author mpolden */ @@ -36,17 +23,6 @@ public class InstanceSerializer { private static final String INSTANCE_ID_FIELD = "instanceId"; private static final String X509_CERTIFICATE_FIELD = "x509Certificate"; - private static final String IDD_SIGNATURE_FIELD = "signature"; - private static final String IDD_SIGNING_KEY_VERSION_FIELD = "signing-key-version"; - private static final String IDD_PROVIDER_UNIQUE_ID_FIELD = "provider-unique-id"; - private static final String IDD_PROVIDER_SERVICE_FIELD = "provider-service"; - private static final String IDD_DOCUMENT_VERSION_FIELD = "document-version"; - private static final String IDD_CONFIGSERVER_HOSTNAME_FIELD = "configserver-hostname"; - private static final String IDD_INSTANCE_HOSTNAME_FIELD = "instance-hostname"; - private static final String IDD_CREATED_AT_FIELD = "created-at"; - private static final String IDD_IPADDRESSES_FIELD = "ip-addresses"; - private static final String IDD_IDENTITY_TYPE_FIELD = "identity-type"; - private InstanceSerializer() {} public static InstanceRegistration registrationFromSlime(Slime slime) { @@ -54,7 +30,7 @@ public class InstanceSerializer { return new InstanceRegistration(requireField(PROVIDER_FIELD, root).asString(), requireField(DOMAIN_FIELD, root).asString(), requireField(SERVICE_FIELD, root).asString(), - attestationDataToIdentityDocument(StringUtilities.unescape(requireField(ATTESTATION_DATA_FIELD, root).asString())), + requireField(ATTESTATION_DATA_FIELD, root).asString(), Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString())); } @@ -75,33 +51,6 @@ public class InstanceSerializer { return slime; } - public static SignedIdentityDocument attestationDataToIdentityDocument(String attestationData) { - Slime slime = SlimeUtils.jsonToSlime(attestationData); - Cursor root = slime.get(); - String signature = requireField(IDD_SIGNATURE_FIELD, root).asString(); - long signingKeyVersion = requireField(IDD_SIGNING_KEY_VERSION_FIELD, root).asLong(); - VespaUniqueInstanceId providerUniqueId = VespaUniqueInstanceId.fromDottedString(requireField(IDD_PROVIDER_UNIQUE_ID_FIELD, root).asString()); - AthenzService athenzService = new AthenzService(requireField(IDD_PROVIDER_SERVICE_FIELD, root).asString()); - long documentVersion = requireField(IDD_DOCUMENT_VERSION_FIELD, root).asLong(); - String configserverHostname = requireField(IDD_CONFIGSERVER_HOSTNAME_FIELD, root).asString(); - String instanceHostname = requireField(IDD_INSTANCE_HOSTNAME_FIELD, root).asString(); - double createdAtTimestamp = requireField(IDD_CREATED_AT_FIELD, root).asDouble(); - Instant createdAt = getJsr310Instant(createdAtTimestamp); - Set<String> ips = new HashSet<>(); - requireField(IDD_IPADDRESSES_FIELD, root).traverse((ArrayTraverser) (__, entry) -> ips.add(entry.asString())); - IdentityType identityType = IdentityType.fromId(requireField(IDD_IDENTITY_TYPE_FIELD, root).asString()); - - return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion, - configserverHostname, instanceHostname, createdAt, ips, identityType); - } - - private static Instant getJsr310Instant(double v) { - var val = new BigDecimal(v); - var seconds = val.longValue(); - var nanos = val.subtract(new BigDecimal(seconds)).scaleByPowerOfTen(9).longValue(); - return Instant.ofEpochSecond(seconds, nanos).truncatedTo(ChronoUnit.MILLIS); - } - private static Cursor requireField(String fieldName, Cursor root) { var field = root.field(fieldName); if (!field.valid()) throw new IllegalArgumentException("Missing required field '" + fieldName + "'"); |