Class ExternalIssuerAdmission

java.lang.Object
co.ankatech.ankasecure.sdk.auth.ExternalIssuerAdmission

public final class ExternalIssuerAdmission extends Object
Decides whether this process may talk to an external authorization server at all, and refuses locally - before a socket exists - when it may not.

Why the decision is made here rather than at connect time

On this path the issuer is configuration: an operator writes it, and after that the SDK posts a credential to whatever it names. A rule enforced when the connection is attempted is a rule enforced after DNS, after TCP and after a TLS handshake with the host in question - which is already an outbound signal, already a reachability probe, and already enough to be useful to whoever chose the name. Admission happens once, before anything, and its refusals cost no packets.

The rules

  1. parseable as an HTTP URL at all;
  2. https - a token crosses this hop;
  3. no userinfo. Checked BEFORE the URI is retained anywhere, because the description of an issuer flows into log lines, exception messages and diagnostic context maps;
  4. no query and no fragment - an issuer is an origin plus a path, and anything else is either a mistake or an attempt to smuggle a parameter into a URL that gets extended later;
  5. the host resolves, once, and EVERY address it resolves to is outside the egress reject set (RejectedEgressAddresses.isRejectedEgressTarget(InetAddress)).

Why resolution happens here and not only literal screening

Screening the literal text catches https://169.254.169.254/ and nothing else. A name whose A record is the cloud metadata address passes every textual rule that could be written, so the shape of a name does not decide it - only resolving it does. The workspace's own rule for this is that resolution belongs with the caller that will also PIN what it resolved, which is why admit(String) returns the addresses rather than a verdict: a caller holding them has no reason, and no way, to look the name up a second time.

The private-address opt-in

"ANKASECURE_ALLOW_PRIVATE_EXTERNAL_ISSUER"="1" in the PROCESS ENVIRONMENT admits an issuer whose every address is LOOPBACK - the local Keycloak an integrator develops against. Two deliberate narrowings:

  • It is read with System.getenv(String) directly and is absent from AuthSetting, matching CommandExternalTokenProvider.ALLOW_ENV_VARIABLE. A properties file must never be able to widen where this process posts a credential, and the injectable environment seam on AuthSettings must never reach it either - the point is precisely that no caller can supply its own answer.
  • It admits LOOPBACK only. RFC 1918, link-local, CGNAT, unique-local and the transition prefixes stay refused with the opt-in set, because the environment of a container is not a trusted input either: an ENV line in an image would otherwise open that image's entire internal network, and a developer's local issuer is on loopback.
  • Field Details

    • ALLOW_PRIVATE_ENV_VARIABLE

      public static final String ALLOW_PRIVATE_ENV_VARIABLE
      The one variable that admits a loopback issuer. Deliberately absent from AuthSetting.
      See Also:
    • ALLOW_PRIVATE_ENV_VALUE

      public static final String ALLOW_PRIVATE_ENV_VALUE
      The one value that enables it. Anything else - including true - does not.
      See Also:
  • Method Details

    • admit

      public static AdmittedIssuer admit(String issuer)
      Admits an external issuer, or refuses it locally.
      Parameters:
      issuer - the configured issuer URL
      Returns:
      the admitted issuer, carrying the addresses a caller must dial
      Throws:
      ExternalIssuerAdmissionException - naming the rule that refused it. No connection is opened on any path through this method other than the name resolution it performs once.
    • screenIssuerText

      public static String screenIssuerText(String issuer)
      Applies the literal rules -- everything decidable from the text -- and nothing else.

      This is the half of admit(String) that opens no socket and looks up no name, so it is the half a configuration-time caller may run. init in the crypto CLI is the one in the workspace: it declares an issuer into a file, offline, and must refuse the values the first authentication would refuse -- but it must not resolve a host to do it, because a declaration is not a connection and a laptop configuring a deployment it cannot reach is a legitimate case.

      It exists so that caller does not re-implement these rules. A second hand-written copy cannot be kept in step: the CLI's own java.net.URI version refused every internationalized domain name that this one admits, which is the init command rejecting issuers the SDK would have accepted -- a refusal in the wrong direction, and invisible until someone measured both parsers against the same input.

      The return value is deliberately String and not the parsed URL: the parser is an implementation detail of this class, and putting it in a signature would export it.

      Parameters:
      issuer - the configured issuer URL
      Returns:
      the issuer as this class parsed it, which may differ from the input in case and in punycode encoding, and never carries userinfo, query or fragment
      Throws:
      ExternalIssuerAdmissionException - naming the rule that refused it. No connection is opened and no name is resolved on any path.
    • privateIssuerAllowed

      public static boolean privateIssuerAllowed()
      Whether this process environment permits a loopback external issuer.

      Reads System.getenv(String) directly, at the point of use, and takes no argument. That is the control: nothing a caller passes in, and nothing a properties file contains, can change the answer.

      Returns:
      true only when the variable is present with exactly the enabling value