Documentation
¶
Overview ¶
Package httpserver provides HTTP API handlers for HTCondor operations.
Package httpserver provides HTTP API handlers for HTCondor operations.
Index ¶
- Constants
- Variables
- func AuthenticatedViaAPIKey(ctx context.Context) bool
- func ConfigureSecurityForCollectorPing(token, serverName string) (*security.SecurityConfig, error)
- func ConfigureSecurityForToken(token string) (*security.SecurityConfig, error)
- func ConfigureSecurityForTokenWithCache(token string, sessionCache *security.SessionCache) (*security.SecurityConfig, error)
- func ConfigureSecurityForTokenWithCacheAndFallback(token string, sessionCache *security.SessionCache, allowFSFallback bool) (*security.SecurityConfig, error)
- func ContainsScope(ctx context.Context, scope string) bool
- func GenerateSigningKey() ([]byte, error)
- func GetScheddWithToken(ctx context.Context, schedd *htcondor.Schedd) (*htcondor.Schedd, error)
- func GetSecurityConfigFromToken(ctx context.Context) (*security.SecurityConfig, error)
- func GetTokenFromContext(ctx context.Context) (string, bool)
- func OAuth2CallbackPath() string
- func ParseGroupSources(raw string) ([]string, error)
- func ParseTokenRetention(raw string) (time.Duration, error)
- func WithRequestedRedirectURI(ctx context.Context, uri string) context.Context
- func WithToken(ctx context.Context, token string) context.Context
- type AdminClient
- type AdminClientUse
- type AdminCondorConfigEntry
- type AdminCondorConfigResponse
- type AdminLogsResponse
- type AdminRevokeRequest
- type AdminRevokeResponse
- type AdminRevokeTokenRequest
- type AdminRevokeTokenResponse
- type AdminSetTokenScopesRequest
- type AdminSetTokenScopesResponse
- type AdminToken
- type AdvertiseRequest
- type AdvertiseResponse
- type AuthMeResponse
- type ClientOrigin
- type CollectorAdsResponse
- type Config
- type DagGraphGroup
- type DagGraphNode
- type DagGraphResponse
- type DashboardActivity
- type DashboardActivityResponse
- type DashboardResponse
- type DeviceAuthorizationResponse
- type DeviceCodeHandler
- func (h *DeviceCodeHandler) HandleDeviceAccessRequest(ctx context.Context, deviceCode string, session fosite.Session) (fosite.Requester, error)
- func (h *DeviceCodeHandler) HandleDeviceAuthorizationRequest(ctx context.Context, client fosite.Client, scopes []string) (*DeviceAuthorizationResponse, error)
- type ErrorResponse
- type ExitCodeCount
- type GoodputSummary
- type GrantRef
- type Handler
- func (h *Handler) AdvertiseAugment() func(*classad.ClassAd)
- func (h *Handler) GetOAuth2Provider() *OAuth2Provider
- func (h *Handler) GetSchedd() *htcondor.Schedd
- func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (h *Handler) SetMCPAccessGroups(raw string)
- func (h *Handler) SetMCPAdminGroups(raw string)
- func (h *Handler) SetMCPDisabledTools(spec string)
- func (h *Handler) SetMCPInstructions(instructions string)
- func (h *Handler) SetMCPReadGroups(raw string)
- func (h *Handler) SetMCPSkillsDir(dir string)
- func (h *Handler) SetMCPSuperuserGroups(raw string)
- func (h *Handler) SetMCPWriteGroups(raw string)
- func (h *Handler) SetSuperuserGroups(raw string)
- func (h *Handler) SetWebUIAccessGroups(raw string)
- func (h *Handler) SetWebUIAdminGroups(raw string)
- func (h *Handler) SetupRoutes(setupFunc func(*http.ServeMux))
- func (h *Handler) Start(ctx context.Context, ln net.Listener, protocol string) error
- func (h *Handler) Stop(ctx context.Context) error
- func (h *Handler) UpdateOAuth2RedirectURL(redirectURL string)
- func (h *Handler) UpdateSchedd(newAddress string)
- type HandlerConfig
- type HistoryListResponse
- type HoldReasonCount
- type IDPProvider
- type IDPProviderOptions
- type IDPStorage
- func (s *IDPStorage) AuthenticateUser(ctx context.Context, username, password string) error
- func (s *IDPStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error
- func (s *IDPStorage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *IDPStorage) CreateAuthorizeCodeSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *IDPStorage) CreateClient(ctx context.Context, client *fosite.DefaultClient) error
- func (s *IDPStorage) CreateOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) error
- func (s *IDPStorage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *IDPStorage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error
- func (s *IDPStorage) CreateSession(ctx context.Context, username string) (string, error)
- func (s *IDPStorage) CreateUser(ctx context.Context, username, password, state string) error
- func (s *IDPStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error
- func (s *IDPStorage) DeleteOpenIDConnectSession(ctx context.Context, signature string) error
- func (s *IDPStorage) DeletePKCERequestSession(ctx context.Context, signature string) error
- func (s *IDPStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
- func (s *IDPStorage) DeleteSession(ctx context.Context, sessionID string) error
- func (s *IDPStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *IDPStorage) GetAuthorizeCodeSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *IDPStorage) GetClient(ctx context.Context, clientID string) (fosite.Client, error)
- func (s *IDPStorage) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (fosite.Requester, error)
- func (s *IDPStorage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *IDPStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *IDPStorage) GetSession(ctx context.Context, sessionID string) (string, error)
- func (s *IDPStorage) GetUserState(ctx context.Context, username string) (string, error)
- func (s *IDPStorage) InvalidateAuthorizeCodeSession(ctx context.Context, signature string) error
- func (s *IDPStorage) LoadHMACSecret(ctx context.Context) ([]byte, error)
- func (s *IDPStorage) LoadRSAKey(ctx context.Context) (string, error)
- func (s *IDPStorage) RevokeAccessToken(ctx context.Context, requestID string) error
- func (s *IDPStorage) RevokeAllForSubject(ctx context.Context, subject string) (int64, error)
- func (s *IDPStorage) RevokeRefreshToken(ctx context.Context, requestID string) error
- func (s *IDPStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
- func (s *IDPStorage) RotateRefreshToken(ctx context.Context, requestID string, _ string) error
- func (s *IDPStorage) SaveHMACSecret(ctx context.Context, secret []byte) error
- func (s *IDPStorage) SaveRSAKey(ctx context.Context, privateKeyPEM string) error
- func (s *IDPStorage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error
- func (s *IDPStorage) SetSealer(sealer *seal.Sealer)
- func (s *IDPStorage) UserExists(ctx context.Context, username string) (bool, error)
- type Impersonation
- type InteractiveCreateTerminalRequest
- type InteractiveCreateTerminalResponse
- type InteractiveTerminalSummary
- type IssueSection
- type IssueTimings
- type IssuesResponse
- type JobActionFunc
- type JobEditRequest
- type JobListResponse
- type JobLogResponse
- type JobSubmitRequest
- type JobSubmitResponse
- type JupyterCreateRequest
- type JupyterCreateResponse
- type JupyterInstanceSummary
- type LoginRateLimiter
- type OAuth2Provider
- func (p *OAuth2Provider) AuthenticateClient(ctx context.Context, r *http.Request, form url.Values) (fosite.Client, error)
- func (p *OAuth2Provider) Close() error
- func (p *OAuth2Provider) GetProvider() fosite.OAuth2Provider
- func (p *OAuth2Provider) GetStorage() *OAuth2Storage
- func (p *OAuth2Provider) GetStrategy() *compose.CommonStrategy
- func (p *OAuth2Provider) IntrospectAccessToken(ctx context.Context, token string) (fosite.AccessRequester, error)
- func (p *OAuth2Provider) IntrospectToken(ctx context.Context, token string) (fosite.Session, error)
- func (p *OAuth2Provider) UpdateIssuer(issuer string)
- type OAuth2ProviderOptions
- type OAuth2StateEntry
- type OAuth2StateStore
- func (s *OAuth2StateStore) GenerateState() (string, error)
- func (s *OAuth2StateStore) Get(state string) (fosite.AuthorizeRequester, bool)
- func (s *OAuth2StateStore) GetWithURL(state string) (fosite.AuthorizeRequester, string, bool)
- func (s *OAuth2StateStore) GetWithUsername(state string) (fosite.AuthorizeRequester, string, []string, bool)
- func (s *OAuth2StateStore) Remove(state string)
- func (s *OAuth2StateStore) Start(ctx context.Context)
- func (s *OAuth2StateStore) Store(state string, ar fosite.AuthorizeRequester)
- func (s *OAuth2StateStore) StoreWithURL(state string, ar fosite.AuthorizeRequester, originalURL string)
- func (s *OAuth2StateStore) StoreWithUsername(state string, ar fosite.AuthorizeRequester, originalURL, username string, ...)
- func (s *OAuth2StateStore) Wait()
- type OAuth2Storage
- func (s *OAuth2Storage) ApproveDeviceCodeSession(ctx context.Context, userCode string, subject string, session fosite.Session) error
- func (s *OAuth2Storage) ApproveDeviceCodeSessionWithScopes(ctx context.Context, userCode string, subject string, session fosite.Session, ...) error
- func (s *OAuth2Storage) ClientAssertionJWTValid(ctx context.Context, jti string) error
- func (s *OAuth2Storage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *OAuth2Storage) CreateAuthorizeCodeSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *OAuth2Storage) CreateClient(ctx context.Context, client *fosite.DefaultClient) error
- func (s *OAuth2Storage) CreateDeviceCodeSession(ctx context.Context, deviceCode string, userCode string, ...) error
- func (s *OAuth2Storage) CreateOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) error
- func (s *OAuth2Storage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *OAuth2Storage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error
- func (s *OAuth2Storage) DeleteAccessTokenSession(ctx context.Context, signature string) error
- func (s *OAuth2Storage) DeleteOpenIDConnectSession(ctx context.Context, signature string) error
- func (s *OAuth2Storage) DeletePKCERequestSession(ctx context.Context, signature string) error
- func (s *OAuth2Storage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
- func (s *OAuth2Storage) DenyDeviceCodeSession(ctx context.Context, userCode string) error
- func (s *OAuth2Storage) EnsureGrantAuthorizedScopes(ctx context.Context, requestID string, scopes []string) error
- func (s *OAuth2Storage) FindGrantBySignaturePrefix(ctx context.Context, kind, prefix string) (GrantRef, error)
- func (s *OAuth2Storage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *OAuth2Storage) GetAuthorizeCodeSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *OAuth2Storage) GetClient(ctx context.Context, clientID string) (fosite.Client, error)
- func (s *OAuth2Storage) GetDB() *sql.DB
- func (s *OAuth2Storage) GetDeviceCodeSession(ctx context.Context, deviceCode string, session fosite.Session) (fosite.Requester, error)
- func (s *OAuth2Storage) GetDeviceCodeSessionByUserCode(ctx context.Context, userCode string) (string, fosite.Requester, error)
- func (s *OAuth2Storage) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (fosite.Requester, error)
- func (s *OAuth2Storage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *OAuth2Storage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
- func (s *OAuth2Storage) GrantAuthorizedScopes(ctx context.Context, requestID string) ([]string, error)
- func (s *OAuth2Storage) GrantScopes(ctx context.Context, requestID string) ([]string, error)
- func (s *OAuth2Storage) InvalidateAuthorizeCodeSession(ctx context.Context, signature string) error
- func (s *OAuth2Storage) InvalidateDeviceCodeSession(ctx context.Context, deviceCode string) error
- func (s *OAuth2Storage) LoadHMACSecret(ctx context.Context) ([]byte, error)
- func (s *OAuth2Storage) LoadRSAKey(ctx context.Context) (string, error)
- func (s *OAuth2Storage) RevokeAccessToken(ctx context.Context, requestID string) error
- func (s *OAuth2Storage) RevokeAllForSubject(ctx context.Context, subject string) (int64, error)
- func (s *OAuth2Storage) RevokeGrant(ctx context.Context, requestID string) (int64, error)
- func (s *OAuth2Storage) RevokeRefreshToken(ctx context.Context, requestID string) error
- func (s *OAuth2Storage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
- func (s *OAuth2Storage) RotateRefreshToken(ctx context.Context, requestID string, _ string) error
- func (s *OAuth2Storage) SaveHMACSecret(ctx context.Context, secret []byte) error
- func (s *OAuth2Storage) SaveRSAKey(ctx context.Context, privateKeyPEM string) error
- func (s *OAuth2Storage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error
- func (s *OAuth2Storage) SetGrantScopes(ctx context.Context, requestID string, scopes []string) (int64, error)
- func (s *OAuth2Storage) SetSealer(sealer *seal.Sealer)
- func (s *OAuth2Storage) UpdateDeviceCodePolling(ctx context.Context, deviceCode string) error
- type PeekResponse
- type PeekedStreamResponse
- type PingResponse
- type ReauthDecision
- type RecentJob
- type RevocationOracle
- type ScheddACLOracle
- type Server
- func (s *Server) GetAddr() string
- func (s *Server) ServeAdditionalListener(ln net.Listener, certFile, keyFile string) error
- func (s *Server) ServeListener(ln net.Listener, scheme string) error
- func (s *Server) ServeListenerWithCert(ln net.Listener, certFile, keyFile string) error
- func (s *Server) ServeMCPListener(ln net.Listener, certFile, keyFile string) error
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Start() error
- func (s *Server) StartTLS(certFile, keyFile string) error
- type Session
- type SessionData
- type SessionStore
- type ShareInputResponse
- type ShareInputUpload
- type ShareOutputRequest
- type ShareOutputResponse
- type ShareWatchRequest
- type ShareWatchResponse
- type SharedInputResult
- type SuperuserModeRequest
- type SuperuserModeResponse
- type TokenCache
- func (tc *TokenCache) Add(token string) (*TokenCacheEntry, error)
- func (tc *TokenCache) AddValidated(token, username string, expiration time.Time) (*TokenCacheEntry, error)
- func (tc *TokenCache) Get(token string) (*TokenCacheEntry, bool)
- func (tc *TokenCache) MarkValidated(token, authoritativeUsername string)
- func (tc *TokenCache) Remove(token string)
- func (tc *TokenCache) Size() int
- func (tc *TokenCache) ValidatedUsername(token string) string
- type TokenCacheEntry
- type UpstreamRefreshMode
- type UserInfo
- type UserRecordLookup
- type UserRecordOracle
- type UserStatus
- type VersionResponse
- type WhoAmIResponse
Constants ¶
const ( InteractiveWatchdogPollSec = interactive.DefaultTerminalWatchdogPollSec InteractiveWatchdogFreshnessSec = interactive.DefaultTerminalWatchdogFreshnessSec )
Watchdog timing for browser terminals, and how often the bridge heartbeats them.
The watchdog polls every InteractiveWatchdogPollSec; if .heartbeat is older than InteractiveWatchdogFreshnessSec it exits. The bridge sends a heartbeat every interactiveHeartbeatIntervalSec for as long as the WebSocket is open, and gives up after interactiveMaxIdleSec without a keystroke.
That idle bound used to be 60s, which is where a bug lived: a user who read output or thought for two minutes with the tab open stopped being heartbeated and lost their shell to the watchdog. The socket being open is the real "someone is there" signal -- a closed tab closes it -- so the keystroke clock now only exists to reclaim a tab left open and forgotten, and is set at the scale that behaviour actually happens on.
const ( // DefaultMCPMaxRequestDuration is how long an MCP request may run while // it keeps making progress. Deliberately generous -- the point is to let // a watch wait for a job rather than for a timeout -- but finite, so a // stuck tool is bounded by something. DefaultMCPMaxRequestDuration = 15 * time.Minute // DefaultDeliverableWatchWait is how long a block can be expected to // SURVIVE, as against how long this server is willing to run it. // // The two are different numbers and only one of them is ours. The // deadline extension above settles what this daemon will hold a request // open for -- 14m30s once the reply margin is taken off the default hard // stop -- and says nothing about the MCP client at the other end, which // abandons a tool call on a timer of its own that this server cannot // see, cannot extend and is not told about. Measured against a live // deployment: a 45s and a 50s block both came back with their answer, // while 120s and 600s returned nothing at all -- not a timeout result, no // payload, just the client giving up. Reports of that timer put it near // 60s, and it varies by client: the CLI honours a per-server override, // the desktop app reportedly ignores it, and a progress notification // does not reset it. // // So a cap derived from our own deadline is a number no one can deliver, // and advertising it is worse than advertising a small one. The tool // description is what an agent plans against; told it may wait 14m30s it // asks for minutes, and every such call returns nothing -- which an agent // cannot tell apart from a lost answer, where a wait that runs out and // says "not yet" is unambiguous and costs one more turn. 45s is the // longest block observed to arrive, with the rest of the minute left for // the evaluation pass and the reply. // // This bounds the DEFAULT only. HTTP_API_MCP_WATCH_MAX_WAIT still wins // outright, in both directions: an operator who knows their clients are // configured for longer, or who has a gateway tighter than this, is the // only one who knows, and setting it is how they say so. DefaultDeliverableWatchWait = 45 * time.Second )
const ( // OracleScheddUserRec honors `condor_qusers -disable`, treating a user // record with no Enabled statement as no opinion. OracleScheddUserRec = "schedd-userrec" // OracleScheddUserRecStrict is OracleScheddUserRec plus "a user with no // record at all is revoked". Only correct in a pool that provisions a // record for every user; see UserRecordOracle.Strict. OracleScheddUserRecStrict = "schedd-userrec-strict" // OracleScheddACL probes the schedd's ACLs with DC_SEC_QUERY. OracleScheddACL = "schedd-acl" // OracleNone disables every oracle. It exists because a config file // cannot express Go's nil-versus-empty-slice distinction: an unset // HTTP_API_OAUTH2_REVOCATION_ORACLES means "use the defaults", so // there has to be a spelling for "I want none of them". Setting it // alongside other names is a configuration error. OracleNone = "none" )
Recognized revocation oracle names. These are the values accepted by HandlerConfig.OAuth2RevocationOracles and by the HTTP_API_OAUTH2_REVOCATION_ORACLES config knob.
const DefaultMaxGrantLifetime = 30 * 24 * time.Hour
DefaultMaxGrantLifetime bounds how long a single consent can be stretched by refreshing. See Handler.oauth2MaxGrantLifetime.
const DefaultTokenRetention = 90 * 24 * time.Hour
DefaultTokenRetention is how long a dead token row is kept.
Ninety days is chosen to outlast the question an operator asks of this page ("what did that client do, and when did we cut it off?") without keeping rows nobody will ever read. Rows still in use are never touched whatever this says: the cutoff applies to tokens that have expired or been revoked.
Variables ¶
var ( ErrAuthorizationPending = &fosite.RFC6749Error{ ErrorField: "authorization_pending", DescriptionField: "The authorization request is still pending", CodeField: http.StatusBadRequest, } ErrSlowDown = &fosite.RFC6749Error{ ErrorField: "slow_down", DescriptionField: "Client is polling too frequently", CodeField: http.StatusBadRequest, } ErrExpiredToken = &fosite.RFC6749Error{ ErrorField: "expired_token", DescriptionField: "The device code has expired", CodeField: http.StatusBadRequest, } )
Device flow error codes (RFC 8628)
var ErrTokenAmbiguous = errors.New("more than one token matches that fingerprint")
ErrTokenAmbiguous reports that a fingerprint matches more than one token. Refusing is the point: the fingerprint is a truncated signature, so acting on "probably that one" would revoke somebody else's access.
var ErrTokenNotFound = errors.New("no token matches that fingerprint")
ErrTokenNotFound reports that no token matches a fingerprint.
Functions ¶
func AuthenticatedViaAPIKey ¶
AuthenticatedViaAPIKey reports whether ctx was set up by the API-key auth path (rather than a JWT, OAuth2 token, or browser session). Some authorization decisions only make sense for API keys (e.g. "API keys must have an explicit scope; sessions don't").
func ConfigureSecurityForCollectorPing ¶
func ConfigureSecurityForCollectorPing(token, serverName string) (*security.SecurityConfig, error)
ConfigureSecurityForCollectorPing builds a SecurityConfig used solely by the periodic collector ping. The collector ping is read-only — we just need *some* mutually agreeable handshake — so this offers both TOKEN and SSL. That's useful when the daemon's token does not match the collector's IssuerKeys (an issuer rotation, a misconfigured TrustDomain, etc.): SSL keeps /readyz green via a path that has nothing to do with JWT signing. The schedd path — which DOES need the token's identity for authz — keeps using TOKEN only.
SSL is always offered (even with no client cert/key on disk) because many collectors permit anonymous SSL: the client only verifies the server's cert and connects as ANONYMOUS@…, which is enough for a read-only ping. Cedar's SSL auth handles empty CertFile/KeyFile as "no client cert presented" and empty CAFile as "use the system trust store" — see cedar/security/ssl_auth.go and cmd/ssl-test/main.go.
serverName is used by cedar's SSL handshake for hostname/SAN verification. Without it, cedar falls back to the literal string "unknown" and verification fails ("certificate is valid for host.example.com, ..., not unknown"). Pass the bare hostname of the collector address — see hostFromCondorAddress in handler.go.
`token` may be empty; in that case only SSL is offered.
func ConfigureSecurityForToken ¶
func ConfigureSecurityForToken(token string) (*security.SecurityConfig, error)
ConfigureSecurityForToken configures security settings to use the provided token This is a helper function to set up cedar's security configuration for TOKEN authentication
func ConfigureSecurityForTokenWithCache ¶
func ConfigureSecurityForTokenWithCache(token string, sessionCache *security.SessionCache) (*security.SecurityConfig, error)
ConfigureSecurityForTokenWithCache configures security settings with an optional session cache If sessionCache is nil, the global cache will be used
func ConfigureSecurityForTokenWithCacheAndFallback ¶
func ConfigureSecurityForTokenWithCacheAndFallback(token string, sessionCache *security.SessionCache, allowFSFallback bool) (*security.SecurityConfig, error)
ConfigureSecurityForTokenWithCacheAndFallback configures security settings with optional session cache and optional FS authentication fallback.
allowFSFallback semantics:
true: APPEND FS to the offered methods. No production call site passes this any more. It was used for user-header mode, on the premise that such a token was "generated locally per request and not signed with anything the schedd recognises" -- which was not true. extractOrGenerateToken signs the header-mode token with the same s.signingKeyPath and s.trustDomain as the session-mode one, so the schedd validates both identically, and appending FS only let FS win the negotiation and hide the caller's identity behind the daemon's OS user. Retained for the tests that pin the append/strip behaviour itself.
false (session/JWT mode): the token IS signed by us with the pool's signing key, the schedd validates it, and its `sub` claim is the user we want recorded as the job Owner. We therefore REMOVE FS from the offered methods, so the schedd can't pick it during negotiation. (Cedar's negotiation walks the server's preference order and selects the first method also offered by the client; HTCondor's default lists FS first, so leaving FS in the client's list lets FS win on a same-host schedd, and the schedd then records the OS user instead of the token's identity. We saw this in session_integration_test.go: jobs submitted via session cookie were owned by `vscode` — the test runner's UID — not by the JWT subject `testuser@trust.domain`.)
Authentication methods otherwise come from SEC_CLIENT_AUTHENTICATION_METHODS / SEC_DEFAULT_AUTHENTICATION_METHODS in the loaded HTCondor configuration. This was previously a hardcoded `[TOKEN]` list, which broke any pool that expects SSL alongside IDTOKENS.
Implementation: delegates to htcondor.NewClientSecurityConfig for the configured-methods-aware base, then applies the FS rule above. Other call sites (file_transfer, schedd_ssh, mcpserver) use NewClientSecurityConfig directly; the httpserver-only allowFSFallback knob lives here so we don't drag it into the root package's API.
func ContainsScope ¶
ContainsScope reports whether the request's API key was minted with the named scope. Returns false when the request was NOT authenticated via an API key (or was authenticated via one with different scopes). Use this in handlers that want to opt into API-key access.
func GenerateSigningKey ¶
GenerateSigningKey generates a new signing key for token generation Returns the key content as bytes
func GetScheddWithToken ¶
GetScheddWithToken creates a schedd connection configured with token authentication This wraps the schedd to use token authentication from context
func GetSecurityConfigFromToken ¶
func GetSecurityConfigFromToken(ctx context.Context) (*security.SecurityConfig, error)
GetSecurityConfigFromToken retrieves the token from context and creates a SecurityConfig This is a convenience function for HTTP handlers to convert context token to SecurityConfig
func GetTokenFromContext ¶
GetTokenFromContext retrieves the token from the context
func OAuth2CallbackPath ¶ added in v0.16.1
func OAuth2CallbackPath() string
OAuth2CallbackPath is the path this server advertises as its redirect URI: the plain one when the web UI is compiled in, the MCP-scoped one otherwise.
Both are always routed. A redirect URI is registered with the upstream identity provider, and an authorization already in flight across a restart or an upgrade comes back to whichever path it was started with; answering only the current one would strand it.
func ParseGroupSources ¶ added in v0.16.3
ParseGroupSources splits the comma-separated HTTP_API_GROUP_SOURCE.
Whitespace around each entry is trimmed, so "system, file:/etc/x" reads the way an administrator would write it.
Mixing "token" with a local source is refused rather than merged. The two are different trust bases -- one is what the identity provider asserted, the other is what this machine's account database says -- and unioning them would mean a provider could add a caller to any group the local policy checks. Choosing between them has to be deliberate.
func ParseTokenRetention ¶ added in v0.18.0
ParseTokenRetention reads HTTP_API_TOKEN_RETENTION.
"0" or "off" keeps rows forever, which is a defensible choice for a deployment whose audit policy says so -- and an explicit one, rather than something reached by leaving a knob unset.
func WithRequestedRedirectURI ¶ added in v0.15.0
WithRequestedRedirectURI notes the redirect_uri of the request being handled, for the loopback allowance described on withLoopbackRedirect.
Types ¶
type AdminClient ¶
type AdminClient struct {
ID string `json:"id"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Public bool `json:"public"`
CreatedAt time.Time `json:"created_at"`
// ServiceSubject is the identity a client_credentials token from this
// client asserts (the IDTOKEN subject the schedd authorizes). Empty unless
// set by an admin; required before client_credentials will issue a token.
ServiceSubject string `json:"service_subject,omitempty"`
// Name is what the client called itself at registration (RFC 7591
// client_name). Empty for seeded clients and for anything registered
// before we started keeping it.
Name string `json:"name,omitempty"`
// Notes is the operator's own annotation, editable from the UI. It
// is the only identifying field available for clients that predate
// provenance tracking.
Notes string `json:"notes,omitempty"`
// Origin is "dynamic", "seeded", or empty for unknown. Empty is not
// the same as "not dynamic" -- it means nobody recorded the answer.
Origin string `json:"origin,omitempty"`
// LastUsedAt is when this client last obtained a token. Absent means
// never, which for a dynamically registered client usually means an
// app registered once and never came back.
//
// Written on a debounced background flush, so it can lag real usage
// by up to a flush interval. It is a "roughly when", not an audit
// record; oauth2_access_tokens has the per-token history.
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
// RecentUsers is a rolling sample of the last few distinct subjects
// to obtain a token through this client, newest first.
RecentUsers []AdminClientUse `json:"recent_users,omitempty"`
// RefreshBlockedBy names what stops this client from ever receiving
// a refresh token, so its users re-authorize on every access-token
// expiry. Empty means nothing does -- or that the client has no
// interactive flow and so has no user to inconvenience.
RefreshBlockedBy []string `json:"refresh_blocked_by,omitempty"`
}
AdminClient is the SPA-facing shape for an OAuth2 client. We mirror only the fields useful for an "audit + cleanup" UI; secrets are never returned (they're hashed in storage anyway, but we still strip them out of the response shape on principle).
type AdminClientUse ¶ added in v0.13.0
AdminClientUse is one entry of a client's recent-users sample.
type AdminCondorConfigEntry ¶
type AdminCondorConfigEntry struct {
Key string `json:"key"`
Value string `json:"value,omitempty"`
Redacted bool `json:"redacted,omitempty"`
// IsDefault reports that this key still holds HTCondor's compiled-in
// value — nothing in this deployment's config files or environment
// touched it. Roughly a thousand of the ~1085 keys in a stock config
// are in this state, which is what makes an unfiltered dump tedious
// to read, so the SPA offers to hide them.
IsDefault bool `json:"is_default,omitempty"`
}
AdminCondorConfigEntry is one (key, value) row from the HTCondor config that we surface to the admin info page. Sensitive values (matched by sensitiveCondorKeyPattern) come back with Redacted=true and an empty Value so the admin can SEE the key is set without the raw value rendering on screen — defense-in-depth even though HTCondor convention is that secrets are paths, not literals.
type AdminCondorConfigResponse ¶
type AdminCondorConfigResponse struct {
Configured bool `json:"configured"`
Entries []AdminCondorConfigEntry `json:"entries"`
// ModifiedCount is how many entries this deployment actually set,
// so the SPA can label its filter without counting client-side.
ModifiedCount int `json:"modified_count"`
}
AdminCondorConfigResponse is the full readout. Configured=false means no HTCondor config object was wired into this server; treat as "feature unavailable on this deployment" client-side.
type AdminLogsResponse ¶
type AdminLogsResponse struct {
Enabled bool `json:"enabled"`
Entries []logging.BufferEntry `json:"entries"`
}
AdminLogsResponse wraps the buffer entries with a hint when the buffer hasn't been initialized — the SPA shows a different empty state for "no logs yet" vs "feature not wired up".
type AdminRevokeRequest ¶ added in v0.13.0
type AdminRevokeRequest struct {
// Subject is the user whose grants should be revoked. Matched exactly
// against the subject recorded on each token row, which is the same
// value the OAuth2 session carries (typically the username claim, not
// the schedd's "user@domain" form).
Subject string `json:"subject"`
}
AdminRevokeRequest is the body of a token revocation request.
type AdminRevokeResponse ¶ added in v0.13.0
type AdminRevokeResponse struct {
Subject string `json:"subject"`
// Revoked counts token rows deactivated across both issuers.
Revoked int64 `json:"revoked"`
// OAuth2 and IDP break that count down by issuer, so an operator can
// tell whether the user held MCP grants, IDP grants, or both.
OAuth2 int64 `json:"oauth2"`
IDP int64 `json:"idp"`
}
AdminRevokeResponse reports what was revoked.
type AdminRevokeTokenRequest ¶ added in v0.17.0
type AdminRevokeTokenRequest struct {
// Kind is "access" or "refresh" -- which table the fingerprint is in.
Kind string `json:"kind"`
// Fingerprint is the redacted signature shown in the listing. The
// trailing "..." may be included or not.
Fingerprint string `json:"fingerprint"`
}
AdminRevokeTokenRequest names one token from the admin listing.
type AdminRevokeTokenResponse ¶ added in v0.17.0
type AdminRevokeTokenResponse struct {
Revoked int64 `json:"revoked"`
ClientID string `json:"client_id"`
Subject string `json:"subject,omitempty"`
}
AdminRevokeTokenResponse reports what the revocation actually hit.
type AdminSetTokenScopesRequest ¶ added in v0.18.0
type AdminSetTokenScopesRequest struct {
// Kind is "access" or "refresh" -- which table the fingerprint is in.
Kind string `json:"kind"`
// Fingerprint is the redacted signature shown in the listing.
Fingerprint string `json:"fingerprint"`
// Scopes is the set to keep. Anything currently granted and absent
// here is removed; anything here that is not currently granted is an
// error rather than an addition.
Scopes []string `json:"scopes"`
}
AdminSetTokenScopesRequest narrows one grant from the admin listing.
type AdminSetTokenScopesResponse ¶ added in v0.18.0
type AdminSetTokenScopesResponse struct {
ClientID string `json:"client_id"`
Subject string `json:"subject,omitempty"`
Scopes []string `json:"scopes"`
Removed []string `json:"removed,omitempty"`
Added []string `json:"added,omitempty"`
Rows int64 `json:"rows"`
}
AdminSetTokenScopesResponse reports what the narrowing actually did.
type AdminToken ¶
type AdminToken struct {
Kind string `json:"kind"` // "access" or "refresh"
SignaturePrefix string `json:"signature_prefix"`
ClientID string `json:"client_id"`
// ClientName and Notes are the client's own label and the operator's
// annotation, carried over from the clients page. A client id is a
// generated string; "OpenClaw MCP" is what somebody reading this page
// is actually looking for.
ClientName string `json:"client_name,omitempty"`
Notes string `json:"notes,omitempty"`
Subject string `json:"subject,omitempty"`
Scopes []string `json:"scopes,omitempty"`
// AuthorizedScopes is everything this authorization ended with. Scopes
// is the subset in force now, so the difference is what an operator
// switched off and may switch back on.
AuthorizedScopes []string `json:"authorized_scopes,omitempty"`
Active bool `json:"active"`
RequestedAt time.Time `json:"requested_at"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
AdminToken is the SPA-facing shape for an OAuth2 access/refresh token row. We never expose the raw token signature — only its prefix as a fingerprint, so admins can correlate against logs without being able to use the token themselves.
type AdvertiseRequest ¶
type AdvertiseRequest struct {
Ad *classad.ClassAd `json:"ad,omitempty"` // Single ad (JSON body)
Command string `json:"command,omitempty"` // Optional UPDATE command (e.g., "UPDATE_STARTD_AD")
WithAck bool `json:"with_ack,omitempty"` // Request acknowledgment
}
AdvertiseRequest represents a request to advertise to the collector
type AdvertiseResponse ¶
type AdvertiseResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Succeeded int `json:"succeeded"` // Number of ads successfully advertised
Failed int `json:"failed"` // Number of ads that failed
Errors []string `json:"errors,omitempty"` // Error messages for failed ads
}
AdvertiseResponse represents the response from advertise
type AuthMeResponse ¶
type AuthMeResponse struct {
Authenticated bool `json:"authenticated"`
Username string `json:"username,omitempty"`
Groups []string `json:"groups,omitempty"`
IsAdmin bool `json:"is_admin"`
// SuperuserAllowed reports that this session MAY arm superuser mode --
// the feature is configured and the session is in the superuser group.
// It says nothing about whether the mode is currently on.
SuperuserAllowed bool `json:"superuser_allowed"`
// SuperuserActive reports that the mode is armed right now. The SPA
// shows its warning banner on this, so every page can tell the user
// that their next action may land on somebody else's job.
SuperuserActive bool `json:"superuser_active"`
// SuperuserExpiresAt is when the mode disarms itself. Surfaced so the
// banner can say how long is left rather than have the mode silently
// lapse mid-task.
SuperuserExpiresAt *time.Time `json:"superuser_expires_at,omitempty"`
// SuperuserIdentity is what actions will be attributed to on the schedd
// while the mode is armed, and SuperuserNote explains it if that is not
// the operator themselves.
SuperuserIdentity string `json:"superuser_identity,omitempty"`
SuperuserNote string `json:"superuser_note,omitempty"`
}
AuthMeResponse describes the currently-authenticated browser session.
The Web UI uses this as its single source of truth for "is the user logged in", who they are, and whether to render admin pages. It is intentionally looser than /api/v1/whoami: it always returns 200 (with Authenticated=false when there is no session) so the SPA can render a landing page without going through error-handling.
type ClientOrigin ¶ added in v0.13.0
type ClientOrigin string
ClientOrigin says how a client came to exist. The empty value is meaningful and is not the same as "not dynamic": it marks a row that predates the column, where the answer was never recorded. Rendering that as "not dynamically registered" would assert something nobody checked.
const ( // ClientOriginUnknown is a row that predates provenance tracking. ClientOriginUnknown ClientOrigin = "" // ClientOriginDynamic is a client that registered itself through // /mcp/oauth2/register (RFC 7591). ClientOriginDynamic ClientOrigin = "dynamic" // ClientOriginSeeded is a client this server created at startup. ClientOriginSeeded ClientOrigin = "seeded" )
type CollectorAdsResponse ¶
CollectorAdsResponse represents collector ads listing response
type Config ¶
type Config struct {
ListenAddr string // Address to listen on (e.g., ":8080")
// MCPListenAddr, when set, serves MCP on its own listener instead of
// alongside the web UI and the REST API. Empty is the default and
// keeps everything on one port.
//
// A separate port is worth having only if the split is real, so the
// protocol endpoint then answers there and not on the main listener.
// The OAuth2 endpoints stay on both: a client that reaches either has
// to be able to finish authenticating.
MCPListenAddr string
ScheddName string // Schedd name
ScheddAddr string // Schedd address (e.g., "127.0.0.1:9618"). If empty, discovered from collector.
// ScheddAddrDiscovered says ScheddAddr was resolved from the collector
// rather than set by an operator.
//
// It matters because the two look identical here and behave
// differently: a pinned address is honoured even when the collector
// disagrees, while a discovered one must follow the collector. The
// daemon resolves the address in main() and passes the result, so
// without this every deployment that only sets SCHEDD_NAME looked
// pinned -- and kept dialling the dead socket of a schedd that had
// restarted, forever. See issue #308.
ScheddAddrDiscovered bool
UserHeader string // HTTP header to extract username from (optional)
// UserHeaderTrustedProxies is the CIDR list from which UserHeader
// is honored. See HandlerConfig.UserHeaderTrustedProxies for
// full docs and the security rationale. Configurable via
// HTTP_API_USER_HEADER_TRUSTED_PROXIES.
UserHeaderTrustedProxies []string
// TrustedProxies lists CIDRs whose forwarded headers are honored for
// access logging. Empty means none are. HTTP_API_TRUSTED_PROXIES.
TrustedProxies []string
// UserHeaderTrustAnyUnsafe disables the trusted-proxy gate and
// honors UserHeader from any source. Demo / test only — see
// HandlerConfig.UserHeaderTrustAnyUnsafe. Configurable via
// HTTP_API_USER_HEADER_TRUST_ANY.
UserHeaderTrustAnyUnsafe bool
SigningKeyPath string // Path to token signing key (optional, for token generation)
TrustDomain string // Trust domain for token issuer (optional; only used if UserHeader is set)
UIDDomain string // UID domain for generated token username (optional; only used if UserHeader is set)
HTTPBaseURL string // Base URL for HTTP API (e.g., "http://localhost:8080") for generating file download links in MCP responses
// MCPBaseURL is the public base URL of the MCP listener, when MCP has
// its own port and that port is published under a different origin
// than the web UI. Empty means MCP is reached at the same base URL as
// everything else, which is true whenever the ports are combined and
// whenever a split is only local.
//
// It exists for one attribute: RFC 9728 requires the protected-resource
// document to name the resource the client asked about, and a client
// that reaches MCP on another origin asked about that origin. Naming
// the web UI's instead makes the document one the client must reject.
MCPBaseURL string
// CCB decides how to reach a daemon behind a Condor Connection Broker:
// on an inbound path of this server's own (see sharedportrouter), or by
// having the broker relay. Set once by the operator and applied to every
// surface that reaches into a running job -- a shell, a session, tailing
// output -- so two of them cannot disagree. Nil keeps cedar's default,
// which only works on a host the execute nodes can reach.
CCB *htcondor.CCBDialer
TLSCertFile string // Path to TLS certificate file (optional, enables HTTPS)
TLSKeyFile string // Path to TLS key file (optional, enables HTTPS)
TLSCACertFile string // Path to TLS CA certificate file (optional, for trusting self-signed certs)
ReadTimeout time.Duration // HTTP read timeout (default: 30s)
WriteTimeout time.Duration // HTTP write timeout (default: 30s)
IdleTimeout time.Duration // HTTP idle timeout (default: 120s)
Collector *htcondor.Collector // Collector for metrics (optional)
// JobQueueLogPath, if set, is the path to the schedd's job_queue.log; the
// server mirrors it into a watch-enabled collection and serves
// /api/v1/jobs/watch (SSE) from it. Empty disables the jobs watch endpoint.
JobQueueLogPath string
EnableMetrics bool // Enable /metrics endpoint (default: true if Collector is set)
MetricsCacheTTL time.Duration // Metrics cache TTL (default: 10s)
// MetricsPublic disables the API-key auth gate on /metrics.
// Configurable via HTTP_API_METRICS_PUBLIC; see HandlerConfig.
MetricsPublic bool
// htcondordb mirror routing; see HandlerConfig for what each does.
DBMirrorName string // HTTP_API_DBMIRROR_NAME
DBMirrorAddress string // HTTP_API_DBMIRROR_ADDRESS
DBMirrorRequired bool // HTTP_API_DBMIRROR_REQUIRED
Logger *logging.Logger // Logger instance (optional, creates default if nil)
JupyterWorkDir string // Per-instance scratch dir for JupyterLab submission artifacts; default <TempDir>/htcondor-api-jupyter
// JupyterMaxLifetimeSec is the wall-clock ceiling on a JupyterLab
// session, and JupyterKernelIdleSec the kernel-idle limit after
// which JupyterLab culls and shuts down. Zero disables either.
JupyterMaxLifetimeSec int
JupyterKernelIdleSec int
// InteractiveExtraSubmit is an optional verbatim block of extra
// HTCondor submit-file directives merged into every
// interactive-terminal and Jupyter job. See
// HandlerConfig.InteractiveExtraSubmit for the trust model and
// full documentation. Configurable via
// HTTP_API_INTERACTIVE_EXTRA_SUBMIT.
InteractiveExtraSubmit string
// DagmanPath is where condor_dagman lives on the access point, for
// the submit_dag tool. See HandlerConfig.DagmanPath. Configurable
// via HTTP_API_DAGMAN_PATH.
DagmanPath string
// DagmanEnvironment is extra environment for the DAGMan manager job.
// See HandlerConfig.DagmanEnvironment. Configurable via
// HTTP_API_DAGMAN_ENVIRONMENT.
DagmanEnvironment map[string]string
// InteractiveRequirements is an optional ClassAd expression ANDed into
// the interactive terminal job's Requirements. See
// HandlerConfig.InteractiveRequirements.
InteractiveRequirements string
// Build is the site's container-build configuration; see
// mcpserver.BuildConfig and HandlerConfig.Build.
Build mcpserver.BuildConfig
// SubmitFileDefaults and SubmitFileOverrides are the site-wide
// submit-file policy applied to EVERY submission -- REST, templates,
// interactive, Jupyter and MCP alike. Defaults apply only where the
// submit file is silent; overrides win over it. See
// HandlerConfig for the trust model.
// DBMirrorTokenSubject overrides the identity the htcondordb token
// asserts. See HandlerConfig.
DBMirrorTokenSubject string
SubmitFileDefaults string
SubmitFileOverrides string
// Batch-submission template paths.
TemplateGlobalPath string // Optional YAML file with operator-curated templates
// TemplateUserStoreDBPath is deprecated; the templates store
// shares the unified DBPath. Kept so existing callers compile.
TemplateUserStoreDBPath string //nolint:unused // back-compat; ignored.
EnableMCP bool // Enable MCP endpoints with OAuth2 (default: false)
// DBPath is the unified SQLite database file. See HandlerConfig.DBPath.
DBPath string
// KEKFilePath enables envelope encryption for long-lived secrets
// in the DB. See HandlerConfig.KEKFilePath.
KEKFilePath string
// OAuth2DBPath is the legacy name for DBPath; kept for back-compat.
OAuth2DBPath string
OAuth2Issuer string // OAuth2 issuer URL (default: listen address)
// MCPCIMDEnabled resolves an https:// MCP client_id as a Client ID Metadata
// Document (public client); MCPCIMDAllowedHosts optionally restricts it.
MCPCIMDEnabled bool
MCPCIMDAllowedHosts []string
// MCPTokenExchangeIssuers: JSON list of trusted external issuers for token
// exchange (HTTP_API_MCP_TOKEN_EXCHANGE_ISSUERS).
MCPTokenExchangeIssuers string
OAuth2ClientID string // OAuth2 client ID for SSO (optional)
OAuth2ClientSecret string // OAuth2 client secret for SSO (optional)
OAuth2AuthURL string // OAuth2 authorization URL for SSO (optional)
OAuth2TokenURL string // OAuth2 token URL for SSO (optional)
OAuth2RedirectURL string // OAuth2 redirect URL for SSO (optional)
OAuth2UserInfoURL string // OAuth2 user info endpoint for SSO (optional)
OAuth2Scopes []string // OAuth2 scopes to request (default: ["openid", "profile", "email"])
OAuth2UsernameClaim string // Claim name for username in token (default: "sub")
OAuth2GroupsClaim string // Claim name for groups in user info (default: "groups")
// OAuth2Requirements is a ClassAd expression evaluated against the
// token's claims at login. Empty means no policy.
OAuth2Requirements string
// IdentityMapStrategies is the ordered list of ways to turn an OIDC
// subject into a local account -- "gecos", "username", or both, as in
// "gecos,username". Empty disables mapping entirely. When set, group
// membership comes from the system rather than the token, and a
// caller that maps to no single account is refused a session.
IdentityMapStrategies []idmap.Strategy
// IdentityGroupSources lists where group membership comes from, as
// parsed specs: "system" and/or "file:<path>". Empty keeps the
// token's groups claim, which is what a container wants because it
// holds no account database to read. Independent of
// IdentityMapStrategies: a deployment may want either, both, or
// neither. Several sources are unioned, the way glibc merges NSS
// services, so a site can carry directory groups and hand-maintained
// ones at once.
IdentityGroupSources []string
// IdentityMapPasswdFile reads accounts from this file instead of
// /etc/passwd. Empty means /etc/passwd, which is the only account
// source that enumerates: the GECOS index cannot list accounts that
// live only in a directory. Accounts it does map are still verified
// against the live database, directory included.
IdentityMapPasswdFile string
// IdentityMapTTL is how long the GECOS index and the group lookups
// are reused. Zero means five minutes.
IdentityMapTTL time.Duration
// IdentityMapStripDomain also tries the local part of a scoped
// subject -- "bockelman@wisc.edu" as "bockelman". Only sound where
// something else constrains which identity providers may log in,
// since the local part is not unique across domains.
IdentityMapStripDomain bool
// OAuth2AccessTokenLifespan / OAuth2RefreshTokenLifespan control how long the
// embedded MCP issuer's tokens are valid. Zero means "use the package default"
// (1h access, 30d refresh). RefreshTokenLifespan must be >= AccessTokenLifespan.
OAuth2AccessTokenLifespan time.Duration
OAuth2RefreshTokenLifespan time.Duration
// OAuth2MaxGrantLifetime caps the total age of a grant measured from
// the consent that created it, regardless of how often it is
// refreshed. Defaults to DefaultMaxGrantLifetime (30 days) if zero.
// See HandlerConfig.OAuth2MaxGrantLifetime.
OAuth2MaxGrantLifetime time.Duration
// OAuth2RevocationOracles selects the refresh-time revocation oracles.
// Nil selects the default set; see HandlerConfig.OAuth2RevocationOracles
// for the recognized names.
OAuth2RevocationOracles []string
MCPAccessGroup string // Group required for any MCP access (empty = all authenticated)
MCPReadGroup string // Group required for read operations (empty = all have read)
MCPWriteGroup string // Group required for write operations (empty = all have write)
// UpstreamRefresh is HTTP_API_UPSTREAM_REFRESH: auto, on or off.
// Decides whether this server keeps the identity provider's refresh
// token so it can ask about a user later. See upstream_refresh.go.
UpstreamRefresh string
// TokenRetention is HTTP_API_TOKEN_RETENTION: how long a dead token
// row is kept before deletion. Empty means the default (90 days),
// "off" keeps them forever. See oauth2_retention.go.
TokenRetention string
// MCPAdminGroup / MCPSuperuserGroup gate the two cross-user MCP
// privileges. Empty means NOBODY, not everybody -- see
// HandlerConfig for why these invert the default above.
MCPAdminGroup string
MCPSuperuserGroup string
ScheddHost string // SCHEDD_HOST: the host (optionally name@host, optionally with a port) whose schedd to use
MCPInstructions string // Server-level instructions provided to all MCP agents (e.g., AP-specific guidance)
// MCPDisabledTools (HTTP_API_MCP_DISABLED_TOOLS) names tools this site cannot
// offer, as path.Match patterns separated by commas or whitespace.
MCPDisabledTools string
// MCPSkillsReloadInterval is how often that directory is re-read so a
// checkout updated underneath this process is noticed without a
// reconfigure. Zero disables the poll.
MCPSkillsReloadInterval time.Duration
// MCPSkillsDir publishes a directory of site-authored Markdown skills
// to agents. Empty disables the feature.
MCPSkillsDir string
MCPAdminUsers []string // Authenticated subjects exempt from the MCP owner-scope wrapper
WebUIAdminGroup string // Group required for Web UI admin pages (empty disables admin UI). Configurable via HTTP_API_WEBUI_ADMIN_GROUP.
// WebUIAccessGroup gates web interface login; empty falls back to
// MCPAccessGroup. Comma-separated.
WebUIAccessGroup string
// SpoolBufferDir is where a cluster-wide upload buffers its tar
// before fanning it out to each proc. Empty selects the system temp
// directory.
SpoolBufferDir string
// SuperuserGroup gates superuser mode. Empty disables it. See
// HandlerConfig.SuperuserGroup -- notably, it is NOT WebUIAdminGroup.
SuperuserGroup string
// SuperuserFallbackIdentity overrides the identity used when the
// operator is not themselves a usable queue superuser. Empty selects
// condor@$(UID_DOMAIN). See HandlerConfig.
SuperuserFallbackIdentity string
EnableIDP bool // Enable built-in IDP (always enabled in demo mode)
// SeedDemoUser seeds a second, non-admin IDP account. Demo mode only;
// see HandlerConfig.SeedDemoUser for why it is not keyed on EnableIDP.
SeedDemoUser bool
// IDPDBPath is deprecated; the IDP shares the unified DBPath.
IDPDBPath string //nolint:unused // back-compat; ignored.
IDPIssuer string // IDP issuer URL (default: listen address)
// IDPAccessTokenLifespan / IDPRefreshTokenLifespan: see OAuth2*Lifespan above.
IDPAccessTokenLifespan time.Duration
IDPRefreshTokenLifespan time.Duration
SessionTTL time.Duration // HTTP session TTL (default: 24h)
HTCondorConfig *config.Config // HTCondor configuration (optional, used for LOCAL_DIR default)
// PingInterval is the periodic collector/schedd ping cadence; zero
// or negative disables it. See HandlerConfig.PingInterval.
PingInterval time.Duration
// MCPWatchMaxWait caps in-call MCP watch_jobs blocking; keep it
// under the front gateway timeout. See HandlerConfig.MCPWatchMaxWait.
MCPWatchMaxWait time.Duration
// MCPMaxRequestDuration is the hard stop on an MCP request that keeps
// making progress. See HandlerConfig.MCPMaxRequestDuration.
MCPMaxRequestDuration time.Duration
// MCPUseSDKTransport serves /mcp with the upstream MCP SDK's transport.
// See HandlerConfig.MCPUseSDKTransport.
MCPUseSDKTransport bool
// RequiredCredentials names the OAuth service credentials that must
// exist before a job may be submitted. See
// HandlerConfig.RequiredCredentials.
RequiredCredentials []string
// CreddAddress pins the credd to talk to. See HandlerConfig.CreddAddress.
CreddAddress string
StreamBufferSize int // Buffer size for streaming queries (default: 100)
StreamWriteTimeout time.Duration // Write timeout for streaming queries (default: 5s)
Token string // Token for daemon authentication (optional)
Credd htcondor.CreddClient // Optional credd client; defaults to in-memory implementation
// Placementd is an optional condor_placementd client; nil means
// "discover one". See HandlerConfig.
Placementd htcondor.PlacementdClient
// LLMAPIKeyFile is the path to a file holding the Anthropic API
// key. Empty disables the chat endpoint. See HandlerConfig.
LLMAPIKeyFile string
// LLMAPIURL is an optional override for the upstream Anthropic
// endpoint, useful when the operator runs an LLM gateway. Empty
// = direct to api.anthropic.com.
LLMAPIURL string
// LLMModel overrides the default model id. Empty = package default.
LLMModel string
// LLMOperatorInstructionsFile is the path to a file with extra
// system-prompt rules the operator wants the chat assistant to
// follow on every turn. Empty disables. See HandlerConfig for
// the file-mode requirement and rationale.
LLMOperatorInstructionsFile string
}
Config holds server configuration
type DagGraphGroup ¶ added in v0.19.0
type DagGraphGroup struct {
ID string `json:"id"`
Label string `json:"label"`
// Description is what the group's nodes run, when it is known. It is
// empty for a workflow read from a DOT file, which carries no submit
// descriptions.
Description string `json:"description"`
Count int `json:"count"`
ParentIDs []string `json:"parent_ids"`
Status map[string]int `json:"status"`
}
DagGraphGroup is one collapsed group plus the state histogram of its members, which is what a drawing colours a shape by.
type DagGraphNode ¶ added in v0.19.0
type DagGraphNode struct {
Name string `json:"name"`
GroupID string `json:"group_id"`
State string `json:"state"`
// JobID, HoldReason and ExitCode are the "why" behind a state, and
// they come from the queue or the archive even when the state itself
// came from the status file -- a node the status file calls an error
// is a number until something says which job failed and how.
JobID string `json:"job_id,omitempty"`
HoldReason string `json:"hold_reason,omitempty"`
ExitCode *int `json:"exit_code,omitempty"`
// Detail is DAGMan's own note about the node, when the status file
// carried one ("idle: 2 held", an error message, "Had an ancestor
// node fail").
Detail string `json:"detail,omitempty"`
Source string `json:"source"`
}
DagGraphNode is one node's state.
type DagGraphResponse ¶ added in v0.19.0
type DagGraphResponse struct {
Cluster int `json:"cluster"`
DagFile string `json:"dag_file"`
// DotFile and StatusFile name the files this answer was read out of,
// so a caller looking at the spool can find them.
DotFile string `json:"dot_file"`
StatusFile string `json:"status_file,omitempty"`
NodeCount int `json:"node_count"`
EdgeCount int `json:"edge_count"`
GroupCount int `json:"group_count"`
Groups []DagGraphGroup `json:"groups"`
// Nodes is the per-node overlay, or null when the workflow is too
// large to list node by node. NodesOmitted then says so and
// NodesOmittedReason says why.
Nodes []DagGraphNode `json:"nodes"`
NodesOmitted bool `json:"nodes_omitted,omitempty"`
NodesOmittedReason string `json:"nodes_omitted_reason,omitempty"`
// ApproximateLayering is set when the grouping is not the exact
// structural one -- a cycle, or a refinement that hit its bound -- so
// the group layering is a best effort rather than a topology.
ApproximateLayering bool `json:"approximate_layering,omitempty"`
// StateSources names which of status-file/dot-file/queue/archive
// actually contributed a node state.
StateSources []string `json:"state_sources"`
// StatusFileTime is the node status file's own timestamp, present
// only when that file contributed. It matters: the queue half of this
// response is live, while the status file is only as fresh as DAGMan's
// last write plus the last whole-sandbox fetch.
StatusFileTime int64 `json:"status_file_time,omitempty"`
// Warnings carry what could not be consulted, so a missing archive
// reads as "not available" rather than as "nothing ran".
Warnings []string `json:"warnings,omitempty"`
// FetchedAt is when the structure and the node states in this answer
// were actually read out of the spool -- NOT when this response was
// assembled. On a cached load those are minutes apart, and the panel
// reports this as the age of what it is drawing.
FetchedAt time.Time `json:"fetched_at"`
// TookMS is how long this request spent producing the answer, which
// is the number the panel shows and the one an operator compares a
// cached load against a refresh with.
TookMS int64 `json:"took_ms"`
}
DagGraphResponse is the body of GET /api/v1/jobs/{id}/dag.
type DashboardActivity ¶ added in v0.14.1
type DashboardActivity struct {
HoldReasons []HoldReasonCount `json:"hold_reasons,omitempty"`
RecentlySubmitted []RecentJob `json:"recently_submitted,omitempty"`
RecentlyStarted []RecentJob `json:"recently_started,omitempty"`
RecentlyHeld []RecentJob `json:"recently_held,omitempty"`
RecentlyCompleted []RecentJob `json:"recently_completed,omitempty"`
// CompletedAvailable is false when nothing could answer "what
// finished recently". Reported rather than left as an empty list,
// which would read as "nothing finished".
CompletedAvailable bool `json:"completed_available"`
// CompletedPartial says the list came from the queue alone -- the
// handful still in JobStatus == 4 before the reaper destroys them.
// That is minutes of history at best, and a viewer told otherwise
// would read a short list as a quiet access point.
CompletedPartial bool `json:"completed_partial"`
// HoldWindowSeconds is the span the hold breakdown covers. Reported
// rather than assumed: the rows answer "why did jobs BECOME held
// recently", which is a different question from the HELD tile beside
// them, and a reader who takes it for the latter will conclude the
// backlog vanished.
HoldWindowSeconds int64 `json:"hold_window_seconds,omitempty"`
// Source is what answered, and ComputedAt when. A cached snapshot is
// minutes old by design; saying so is the difference between a stale
// number and a wrong one.
Source string `json:"source"`
ComputedAt int64 `json:"computed_at"`
}
DashboardActivity is the "how is this access point doing" half of the dashboard: why jobs are held, and what has changed lately.
type DashboardActivityResponse ¶ added in v0.14.2
type DashboardActivityResponse struct {
Activity DashboardActivity `json:"activity"`
// Goodput is absent where nothing could answer it -- no history
// archive means no rate, and an omitted field says that where a
// zeroed one would read as "everything failed".
Goodput *GoodputSummary `json:"goodput,omitempty"`
}
DashboardActivityResponse is the slow half of the dashboard.
type DashboardResponse ¶
type DashboardResponse struct {
Username string `json:"username"`
JobsByStatus map[string]int `json:"jobs_by_status"`
JobsTotal int `json:"jobs_total"`
// Activity is the "how is this access point doing" half: why jobs
// are held, and what changed recently. Computed from the same walk
// as the counts.
// Activity and Goodput are served by /api/v1/dashboard/activity and
// omitted here. They stay on the type so a client that has not moved
// yet gets a response missing two optional fields rather than one
// that fails to decode.
Activity DashboardActivity `json:"activity,omitzero"`
// Goodput is absent where nothing could answer it -- no history
// archive means no rate, and an omitted field says that where a
// zeroed one would read as "everything failed".
Goodput *GoodputSummary `json:"goodput,omitempty"`
}
DashboardResponse summarizes the user's queue at the AP. It is a minimal shape on purpose; we'll grow it (transfer history, recent completions, user-level quota) in PR (b)/(c) once the SPA has the basics.
type DeviceAuthorizationResponse ¶
type DeviceAuthorizationResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval,omitempty"`
}
DeviceAuthorizationResponse represents the response from device authorization endpoint
type DeviceCodeHandler ¶
type DeviceCodeHandler struct {
// contains filtered or unexported fields
}
DeviceCodeHandler implements the OAuth 2.0 Device Authorization Grant (RFC 8628)
func NewDeviceCodeHandler ¶
func NewDeviceCodeHandler(storage *OAuth2Storage, config *fosite.Config) *DeviceCodeHandler
NewDeviceCodeHandler creates a new device code handler
func (*DeviceCodeHandler) HandleDeviceAccessRequest ¶
func (h *DeviceCodeHandler) HandleDeviceAccessRequest(ctx context.Context, deviceCode string, session fosite.Session) (fosite.Requester, error)
HandleDeviceAccessRequest handles token requests with device_code grant type
func (*DeviceCodeHandler) HandleDeviceAuthorizationRequest ¶
func (h *DeviceCodeHandler) HandleDeviceAuthorizationRequest(ctx context.Context, client fosite.Client, scopes []string) (*DeviceAuthorizationResponse, error)
HandleDeviceAuthorizationRequest handles the device authorization endpoint
type ErrorResponse ¶
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
Code int `json:"code"`
}
ErrorResponse represents an error response body
type ExitCodeCount ¶ added in v0.14.1
type ExitCodeCount struct {
// Code is the exit status. Signal is set instead when the job was
// killed, in which case Code has no meaning.
Code int64 `json:"code"`
Signal bool `json:"signal"`
Count int `json:"count"`
// Seconds is the wall clock spent on jobs that ended this way.
Seconds int64 `json:"seconds"`
}
ExitCodeCount is one way jobs finished badly.
type GoodputSummary ¶ added in v0.14.1
type GoodputSummary struct {
WindowHours int `json:"window_hours"`
// Since is the exact lower bound the numbers were computed over, so
// a drill-down can ask the archive the same question and get a list
// whose length matches the count that was clicked. Deriving it in
// the browser from WindowHours would drift by the age of the
// response.
Since int64 `json:"since"`
// Succeeded is jobs that ran to completion and said so: exit 0, not
// killed by a signal.
Succeeded int `json:"succeeded"`
// Failed is jobs that ran to completion and reported a problem --
// a non-zero exit or a fatal signal.
Failed int `json:"failed"`
// Unfinished is jobs that left without an outcome, which is what a
// removal looks like from here. Counted separately rather than
// folded into failures: a job its owner cancelled is not the access
// point going wrong.
Unfinished int `json:"unfinished"`
// GoodSeconds and BadSeconds are wall-clock time on the execute
// node, split the same way. This is the half that makes the panel
// worth having.
GoodSeconds int64 `json:"good_seconds"`
BadSeconds int64 `json:"bad_seconds"`
// TopFailures ranks the exit codes behind Failed, because "412 jobs
// failed" and "412 jobs failed with exit 127" are different amounts
// of information.
TopFailures []ExitCodeCount `json:"top_failures,omitempty"`
}
GoodputSummary is the outcome of everything that finished in the window.
type GrantRef ¶ added in v0.17.0
GrantRef identifies one authorization grant: the pairing of an access token with the refresh token minted alongside it.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler represents the HTTP API handler that can be embedded in any HTTP server
func NewHandler ¶
func NewHandler(cfg HandlerConfig) (*Handler, error)
NewHandler creates a new HTTP API handler that can be embedded in any HTTP server
func (*Handler) AdvertiseAugment ¶ added in v0.14.0
AdvertiseAugment returns the daemon.AdvertiseConfig.Augment callback that adds this API server's attributes to its collector ad. Exported so the daemon bootstrap in package main can wire it without reaching into unexported state.
func (*Handler) GetOAuth2Provider ¶
func (h *Handler) GetOAuth2Provider() *OAuth2Provider
GetOAuth2Provider returns the OAuth2 provider (for testing)
func (*Handler) ServeHTTP ¶
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler interface.
Every request flows through the metrics middleware first so the HTTP request counters / duration histogram / in-flight gauge cover every route uniformly — not just the ones we remembered to wrap in setupRoutes. The middleware short-circuits /metrics itself to avoid Prometheus scrapes self-instrumenting.
Security headers are emitted on every response (see applySecurityHeaders). Setting them at the top wrapper guarantees no route can opt out by accident — handlers further down the stack can still override (e.g. relaxing frame-ancestors for an embeddable widget) but the secure defaults are present until explicitly changed.
The response is also wrapped in a status-capturing writer so we can call tokenCache.MarkValidated when a request bearing a JWT returns 2xx. This is the "lazy validation" pattern: there is no local way to verify the JWT signature (the only authoritative validator is the schedd's CEDAR handshake), so we defer to "the handler completed successfully" as evidence that the schedd accepted the token — and only at that point trust the token's `sub` claim for identity decisions like ownedByMe filtering.
func (*Handler) SetMCPAccessGroups ¶ added in v0.15.0
SetMCPAccessGroups installs the groups required for MCP access.
func (*Handler) SetMCPAdminGroups ¶ added in v0.17.0
SetMCPAdminGroups installs the groups whose members may read every user's jobs through MCP. Emptying it revokes the privilege for everyone -- these two, unlike the read/write groups above, have no fallback to a broader group.
func (*Handler) SetMCPDisabledTools ¶ added in v0.18.0
SetMCPDisabledTools installs the site's disabled-tool patterns.
No-op when MCP is disabled, since there is then no server to tell.
func (*Handler) SetMCPInstructions ¶ added in v0.14.4
SetMCPInstructions installs new deployment-specific MCP instructions. No-op when MCP is disabled, since there is then no server to tell.
Only sessions that initialize after this call see the new text; MCP hands an agent its instructions once, in the initialize response.
func (*Handler) SetMCPReadGroups ¶ added in v0.15.0
SetMCPReadGroups installs the groups required for MCP read access.
func (*Handler) SetMCPSkillsDir ¶ added in v0.15.0
SetMCPSkillsDir reloads the site skill library.
Called on every reconfigure, not only when the configured path changes: the usual reason to reload is that the checkout the path points at has been updated, which a diff of the setting cannot see. Reading a few dozen Markdown files is cheap enough to do unconditionally.
No-op when MCP is disabled, since there is then no server to tell.
func (*Handler) SetMCPSuperuserGroups ¶ added in v0.17.0
SetMCPSuperuserGroups installs the groups whose members may change another user's jobs through MCP.
func (*Handler) SetMCPWriteGroups ¶ added in v0.15.0
SetMCPWriteGroups installs the groups required for MCP write access.
func (*Handler) SetSuperuserGroups ¶ added in v0.15.0
SetSuperuserGroups installs the groups permitted to use superuser mode.
Membership only. Superuser mode builds a policy object and a signing identity at startup, and only when a group was configured then, so this cannot switch the feature ON in a running daemon -- it says so rather than appearing to succeed. Emptying it DOES switch it off, since every check goes through the group list.
func (*Handler) SetWebUIAccessGroups ¶ added in v0.15.0
SetWebUIAccessGroups installs the groups required to log in to the web interface. Emptying it restores the fallback to the MCP access groups.
func (*Handler) SetWebUIAdminGroups ¶ added in v0.15.0
SetWebUIAdminGroups installs the groups required for the admin pages.
Emptying it disables the admin UI, which is what an empty value has always meant, so this can switch the admin surface off without a restart as well as change who reaches it.
func (*Handler) SetupRoutes ¶
SetupRoutes sets up the HTTP routes on the handler's multiplexer This should be called by Server.NewServer or by users who create a Handler directly
func (*Handler) Start ¶
Start initializes the handler and starts background goroutines. The provided context controls the handler's lifetime - when the context is cancelled, the handler will gracefully shut down all background goroutines.
This method should be called by Server.Start() or Server.StartTLS() before serving requests.
func (*Handler) Stop ¶
Stop gracefully stops all background goroutines and closes providers. This method is called when the handler's context is cancelled (via Server.Shutdown). The background goroutines are responsible for watching their context and exiting when done.
func (*Handler) UpdateOAuth2RedirectURL ¶
UpdateOAuth2RedirectURL updates the OAuth2 redirect URL for SSO integration
func (*Handler) UpdateSchedd ¶
UpdateSchedd updates the schedd instance with a new address (thread-safe). On change, logs both addresses, the age of the previous address, and — when both addresses are shared-port — the old and new sock= IDs so it's obvious whether a schedd restart drove the update (sock= changed) versus a network-level address shift (host:port changed but sock= stable).
Always sets scheddAddrLastConfirmedAt to now: the caller has just talked to the collector successfully, regardless of whether the address differs from the cached value.
type HandlerConfig ¶
type HandlerConfig struct {
ScheddName string // Schedd name
ScheddAddr string // Schedd address (e.g., "127.0.0.1:9618"). If empty, discovered from collector.
// ScheddAddrDiscovered says ScheddAddr was resolved from the collector
// rather than set by an operator.
//
// It matters because the two look identical here and behave
// differently: a pinned address is honoured even when the collector
// disagrees, while a discovered one must follow the collector. The
// daemon resolves the address in main() and passes the result, so
// without this every deployment that only sets SCHEDD_NAME looked
// pinned -- and kept dialling the dead socket of a schedd that had
// restarted, forever. See issue #308.
ScheddAddrDiscovered bool
// ScheddHost is the SCHEDD_HOST setting: the host (optionally
// "name@host", optionally with a port) whose schedd to talk to.
// Consulted when neither ScheddAddr nor ScheddName is set; it picks
// that host's schedd rather than whichever one the collector
// happens to list first.
ScheddHost string
// InteractiveExtraSubmit holds extra HTCondor submit-file
// directives merged into the submit file produced for each
// interactive-terminal and JupyterLab job. The string value is
// spliced in verbatim just before the `queue` directive, so it
// can override or extend anything the builder emits (typical
// use: pin an accounting group, add +ProjectName, set a
// site-wide `requirements` fragment, force `concurrency_limits`,
// etc.).
//
// Trust model: the value comes from operator-only configuration
// (HTCondor config or env), so it's inserted into every submit
// file verbatim — no whitelist, no quoting. Treat this as the
// operator's hook into job admission policy, equivalent in
// privilege to writing the schedd's site_local config.
//
// Multi-line content is supported via HTCondor config syntax (a
// quoted multi-line value, or backslash continuations). Empty
// disables the feature. Configurable via
// HTTP_API_INTERACTIVE_EXTRA_SUBMIT.
InteractiveExtraSubmit string
// DagmanPath is where condor_dagman lives on the access point
// (HTTP_API_DAGMAN_PATH). Empty means the package default.
DagmanPath string
// DagmanEnvironment is extra environment for the DAGMan manager job
// (HTTP_API_DAGMAN_ENVIRONMENT).
DagmanEnvironment map[string]string
// InteractiveRequirements is an optional ClassAd expression ANDed into
// the interactive terminal job's Requirements.
//
// It exists because a terminal is only useful if it can be attached to,
// and whether that works is a property of the machine. condor_ssh_to_job
// enters the job's namespace with setns, which fails when the container
// runtime made that namespace as root and HTCondor is not root -- the
// job runs, and every attempt to open a shell on it fails. Constraining
// where these jobs land is the only lever the submitter has.
//
// Operator-only configuration, never caller-supplied: it is an
// expression, and a submitter who could set it could widen their own
// match rather than narrow it.
InteractiveRequirements string
// Build is the site's container-build configuration, handed to the
// MCP server's build_container tool. See mcpserver.BuildConfig.
Build mcpserver.BuildConfig
// DBMirrorTokenSubject overrides the identity the htcondordb token
// asserts, default "condor@<trust domain>". Configure via
// HTTP_API_DBMIRROR_TOKEN_SUBJECT.
DBMirrorTokenSubject string
// SubmitFileDefaults are submit-file lines applied to every
// submission ONLY where the submit file is silent, so a user who
// sets the same command keeps their own value. Configure via
// HTTP_API_SUBMIT_FILE_DEFAULTS.
SubmitFileDefaults string
// SubmitFileOverrides are submit-file lines applied to every
// submission that WIN over whatever the submit file says. Use for
// requirements that are not the user's to opt out of -- an access
// point that rejects a `log =` outside the home directory, say.
// Configure via HTTP_API_SUBMIT_FILE_OVERRIDES.
//
// Same trust model as InteractiveExtraSubmit: operator-only config,
// spliced in verbatim.
SubmitFileOverrides string
UserHeader string // HTTP header to extract username from (optional)
// UserHeaderTrustedProxies is a list of CIDRs from which UserHeader
// is honored. When UserHeader is set, this list MUST be non-empty
// (or UserHeaderTrustAnyUnsafe must be true) — otherwise the
// header is silently ignored, treating the request as
// unauthenticated. Configure via HTTP_API_USER_HEADER_TRUSTED_PROXIES
// (comma-separated CIDRs, e.g. "127.0.0.1/32,::1/128,10.0.0.0/8")
// or programmatically.
UserHeaderTrustedProxies []string
// TrustedProxies lists CIDRs (or bare addresses) whose X-Forwarded-For
// and X-Real-IP headers are honoured for access logging. Empty means
// no forwarded header is believed and the peer address is logged.
// Configure via HTTP_API_TRUSTED_PROXIES.
TrustedProxies []string
// UserHeaderTrustAnyUnsafe disables the trusted-proxy check and
// accepts UserHeader from any source. This is the demo / test
// mode only — it is unsafe in any production deployment because
// it lets any client spoof identity by setting the header.
// Configure via HTTP_API_USER_HEADER_TRUST_ANY=1 (loud warning
// at startup).
UserHeaderTrustAnyUnsafe bool
SigningKeyPath string // Path to token signing key (optional, for token generation)
TrustDomain string // Trust domain for token issuer (optional; only used if UserHeader is set)
UIDDomain string // UID domain for generated token username (optional; only used if UserHeader is set)
HTTPBaseURL string // Base URL for HTTP API (e.g., "http://localhost:8080") for generating file download links in MCP responses
// MCPBaseURL is the public base URL of the MCP listener, when MCP has
// its own port and that port is published under a different origin
// than the web UI. Empty means MCP is reached at the same base URL as
// everything else, which is true whenever the ports are combined and
// whenever a split is only local.
//
// It exists for one attribute: RFC 9728 requires the protected-resource
// document to name the resource the client asked about, and a client
// that reaches MCP on another origin asked about that origin. Naming
// the web UI's instead makes the document one the client must reject.
MCPBaseURL string
// CCB decides how to reach a daemon behind a Condor Connection Broker:
// on an inbound path of this server's own (see sharedportrouter), or by
// having the broker relay. Set once by the operator and applied to every
// surface that reaches into a running job -- a shell, a session, tailing
// output -- so two of them cannot disagree. Nil keeps cedar's default,
// which only works on a host the execute nodes can reach.
CCB *htcondor.CCBDialer
TLSCACertFile string // Path to TLS CA certificate file (optional, for trusting self-signed certs)
Collector *htcondor.Collector // Collector for metrics (optional)
// htcondordb mirror routing. Empty/false is the default: discover
// whatever htcondordb advertises to the collector and use it as an
// optional accelerator. See dbmirror.Options for what each does and
// when an operator needs it.
DBMirrorName string // HTTP_API_DBMIRROR_NAME: pin routing to this mirror
DBMirrorAddress string // HTTP_API_DBMIRROR_ADDRESS: dial this sinful instead of the advertised one
DBMirrorRequired bool // HTTP_API_DBMIRROR_REQUIRED: fail rather than fall back to the schedd
JobQueueLogPath string // schedd job_queue.log to mirror for /api/v1/jobs/watch (optional)
EnableMetrics bool // Enable /metrics endpoint (default: true if Collector is set)
MetricsCacheTTL time.Duration // Metrics cache TTL (default: 10s)
// MetricsPublic disables the API-key auth gate on /metrics. Use
// only when network ACLs already isolate the endpoint (e.g. a
// private listening address or a sidecar proxy). Default: false
// — an admin must mint an API key with the `metrics` scope and
// configure Prometheus to send it as a Bearer token.
MetricsPublic bool
Logger *logging.Logger // Logger instance (optional, creates default if nil)
EnableMCP bool // Enable MCP endpoints with OAuth2 (default: false)
// DBPath is the unified SQLite database file backing OAuth2/MCP
// storage, the embedded IDP, browser sessions, and user-saved
// batch-submission templates. Defaults to LOCAL_DIR/htcondor-api.db
// (or /var/lib/condor/htcondor-api.db when LOCAL_DIR is unset).
// Configure via HTTP_API_DB_PATH.
DBPath string
// KEKFilePath is the path to a file holding the master Key
// Encryption Key used to envelope-encrypt long-lived secrets in
// the application database (the OAuth2 / IDP issuer's RSA
// signing key, fosite's HMAC GlobalSecret). Configured via
// HTTP_API_KEK_FILE — the FILE PATH lives in HTCondor config,
// the KEY BYTES never do (HTCondor treats config values as
// public).
//
// Empty disables encryption: secrets are stored in plaintext as
// before. When set, the file must contain exactly 32 raw bytes
// or a 32-byte hex string and must be 0600/0400. See
// httpserver/appdb/seal for the design.
KEKFilePath string
// OAuth2DBPath is a deprecated alias for DBPath kept so existing
// in-process embedders (and the test suite) keep compiling without
// a wholesale rename. NewHandler honors it only when DBPath is
// empty. The cmd-line wrapper does NOT bridge HTTP_API_OAUTH2_DB_PATH
// into this field — pointing the unified DB at a pre-unification
// oauth.db is a guaranteed crash-loop (the legacy schema conflicts
// with goose 0001_init.sql), and the wrapper logs a deprecation
// warning instead. New code should set DBPath.
OAuth2DBPath string
OAuth2Issuer string // OAuth2 issuer URL (default: listen address)
// MCPCIMDEnabled resolves an https:// MCP client_id as a Client ID Metadata
// Document (a public client) instead of requiring DCR. MCPCIMDAllowedHosts,
// when set, restricts which hosts such a client_id may point at.
MCPCIMDEnabled bool
MCPCIMDAllowedHosts []string
// MCPTokenExchangeIssuers is the JSON list of trusted external issuers for
// RFC 8693 token exchange (HTTP_API_MCP_TOKEN_EXCHANGE_ISSUERS). Empty = off.
MCPTokenExchangeIssuers string
OAuth2ClientID string // OAuth2 client ID for SSO (optional)
OAuth2ClientSecret string // OAuth2 client secret for SSO (optional)
OAuth2AuthURL string // OAuth2 authorization URL for SSO (optional)
OAuth2TokenURL string // OAuth2 token URL for SSO (optional)
OAuth2RedirectURL string // OAuth2 redirect URL for SSO (optional)
OAuth2UserInfoURL string // OAuth2 user info endpoint for SSO (optional)
OAuth2Scopes []string // OAuth2 scopes to request (default: ["openid", "profile", "email"])
OAuth2UsernameClaim string // Claim name for username in token (default: "sub")
OAuth2GroupsClaim string // Claim name for groups in user info (default: "groups")
// OAuth2Requirements is a ClassAd expression evaluated against the
// token's claims at login. Empty means no policy.
OAuth2Requirements string
// IdentityMapStrategies is the ordered list of ways to turn an OIDC
// subject into a local account -- "gecos", "username", or both, as in
// "gecos,username". Empty disables mapping entirely. When set, group
// membership comes from the system rather than the token, and a
// caller that maps to no single account is refused a session.
IdentityMapStrategies []idmap.Strategy
// IdentityGroupSources lists where group membership comes from, as
// parsed specs: "system" and/or "file:<path>". Empty keeps the
// token's groups claim, which is what a container wants because it
// holds no account database to read. Independent of
// IdentityMapStrategies: a deployment may want either, both, or
// neither. Several sources are unioned, the way glibc merges NSS
// services, so a site can carry directory groups and hand-maintained
// ones at once.
IdentityGroupSources []string
// IdentityMapPasswdFile reads accounts from this file instead of
// /etc/passwd. Empty means /etc/passwd, which is the only account
// source that enumerates: the GECOS index cannot list accounts that
// live only in a directory. Accounts it does map are still verified
// against the live database, directory included.
IdentityMapPasswdFile string
// IdentityMapTTL is how long the GECOS index and the group lookups
// are reused. Zero means five minutes.
IdentityMapTTL time.Duration
// IdentityMapStripDomain also tries the local part of a scoped
// subject -- "bockelman@wisc.edu" as "bockelman". Only sound where
// something else constrains which identity providers may log in,
// since the local part is not unique across domains.
IdentityMapStripDomain bool
// OAuth2AccessTokenLifespan is how long an access token issued by the embedded
// MCP issuer is valid. Defaults to 1 hour if zero.
OAuth2AccessTokenLifespan time.Duration
// OAuth2RefreshTokenLifespan is how long a refresh token issued by the embedded
// MCP issuer is valid. Defaults to 30 days if zero. Must be >= OAuth2AccessTokenLifespan;
// otherwise refresh grants will fail before the access token expires (see PelicanPlatform/pelican#3389).
OAuth2RefreshTokenLifespan time.Duration
// OAuth2MaxGrantLifetime caps the total age of a grant, measured from
// the consent that created it, regardless of how often it is
// refreshed. Defaults to DefaultMaxGrantLifetime (30 days) if zero.
//
// This is distinct from OAuth2RefreshTokenLifespan, which every
// refresh resets: without a cap, a client that refreshes more often
// than the refresh lifespan holds its access indefinitely, so removing
// a user's entitlement never expires anything. The cap is the backstop
// that bounds that exposure even when no revocation oracle notices the
// removal. See reauthorizeRefreshGrant.
//
// Configurable via HTTP_API_OAUTH2_MAX_GRANT_LIFETIME.
OAuth2MaxGrantLifetime time.Duration
// OAuth2RevocationOracles names the oracles consulted on every refresh
// grant to decide whether the user is still entitled to what they hold.
// Recognized values:
//
// "schedd-userrec" — honor `condor_qusers -disable <user>`. Reads the
// schedd's per-user record (READ authorization) and revokes the
// grant when it says Enabled=false, surfacing DisableReason.
// Cheap, and acts only on an explicit administrative decision.
// "schedd-acl" — probe the schedd's ALLOW_READ / ALLOW_WRITE ACLs
// with DC_SEC_QUERY and strip scopes it refuses. Costs up to two
// extra schedd round trips per refresh and tells you nothing in a
// pool whose ACLs are wildcards, so it is opt-in.
//
// Nil selects the default set (schedd-userrec). An explicitly empty,
// non-nil slice disables all oracles; the grant lifetime cap still
// applies. Unrecognized names are logged and ignored.
//
// Configurable via HTTP_API_OAUTH2_REVOCATION_ORACLES, where the
// literal "none" is the spelling for the empty set — a config file
// cannot express nil-versus-empty on its own.
OAuth2RevocationOracles []string
// SuperuserGroup gates superuser mode, in which an administrator acts
// on another user's jobs as that user. Empty (the default) disables it.
//
// Intentionally separate from WebUIAdminGroup. That group means "may
// read the admin pages"; this one means "may act as anyone on this
// access point", a categorically larger privilege that deserves its own
// decision and its own off switch.
//
// Superuser mode additionally requires a pool signing key: without one
// this server cannot mint the credential it would act under, so the
// feature stays off however this is set.
//
// Configurable via HTTP_API_SUPERUSER_GROUP.
SuperuserGroup string
// SuperuserRefreshInterval is how often the schedd's QUEUE_SUPER_USERS
// set is re-read. Zero selects defaultSuperuserRefresh.
SuperuserRefreshInterval time.Duration
// SuperuserFallbackIdentity is the identity used when the operator is
// not themselves a usable queue superuser. Empty selects
// "condor@$(UID_DOMAIN)".
//
// That default suits a schedd running as the condor user, where
// real_owner_is_condor matches the daemon's own OS user. It does NOT
// suit a personal condor: there personal_condor is true, that branch is
// disabled, and the equivalent identity is the user the pool runs as.
// Deployments that run the schedd as something else need this knob, and
// so does any test that wants to exercise the fallback without root.
//
// Configurable via HTTP_API_SUPERUSER_FALLBACK_IDENTITY.
SuperuserFallbackIdentity string
// SuperuserArmTTL is how long superuser mode stays on before disarming
// itself. Zero selects defaultSuperuserArmTTL.
SuperuserArmTTL time.Duration
MCPAccessGroup string // Group required for any MCP access (empty = all authenticated)
MCPReadGroup string // Group required for read operations (empty = all have read)
MCPWriteGroup string // Group required for write operations (empty = all have write)
// MCPAdminGroup grants the mcp:admin scope -- reading every user's
// jobs through MCP. Empty disables it: unlike MCPReadGroup and
// MCPWriteGroup, an empty value here grants the privilege to NOBODY
// rather than to everybody. HTTP_API_MCP_ADMIN_GROUP.
MCPAdminGroup string
// MCPSuperuserGroup grants the mcp:superuser scope -- changing
// another user's jobs (remove, hold, release, edit). Empty disables.
//
// Separate from MCPAdminGroup for the same reason SuperuserGroup is
// separate from WebUIAdminGroup: seeing every job and being able to
// remove every job are different privileges, and the second deserves
// its own decision and its own off switch.
// HTTP_API_MCP_SUPERUSER_GROUP.
MCPSuperuserGroup string
MCPInstructions string // Server-level instructions provided to all MCP agents (e.g., AP-specific guidance)
// MCPDisabledTools (HTTP_API_MCP_DISABLED_TOOLS) names tools this site cannot
// offer, as path.Match patterns separated by commas or whitespace.
MCPDisabledTools string
// MCPSkillsReloadInterval is how often to re-read MCPSkillsDir.
MCPSkillsReloadInterval time.Duration
// MCPSkillsDir is a directory of site-authored Markdown skills to
// publish to agents. Empty disables the feature.
MCPSkillsDir string
// MCPAdminUsers lists authenticated subjects that MCP tool
// dispatch treats as admins — most importantly they are exempt
// from the owner-scope wrapper, so they can query and act on other
// users' jobs. Match is exact against the authenticated actor
// (typically "user@uid.domain"). Empty = no admins (default).
MCPAdminUsers []string
WebUIAdminGroup string // Group(s) required for Web UI admin pages (empty disables admin UI). Comma-separated; HTTP_API_WEBUI_ADMIN_GROUP.
// WebUIAccessGroup gates logging in to the web interface, separately
// from MCP. Comma-separated; HTTP_API_WEBUI_ACCESS_GROUP. Empty falls
// back to MCPAccessGroup.
WebUIAccessGroup string
EnableIDP bool // Enable built-in IDP (always enabled in demo mode)
// SpoolBufferDir is where cluster-wide uploads buffer their tar.
// Empty selects the system temp directory.
SpoolBufferDir string
// SeedDemoUser additionally seeds a second, non-admin IDP account
// ("user") alongside "admin", printing its generated password the
// same way.
//
// Deliberately NOT keyed on EnableIDP: the built-in IDP can be turned
// on in a real deployment with HTTP_API_ENABLE_IDP, and seeding a
// second standing account there — with its password on stdout — is
// not something an operator asked for. Only -demo sets this.
//
// It exists so tests have two identities to check authorization
// boundaries with: an admin, and someone who is not.
SeedDemoUser bool
// IDPDBPath is deprecated; the IDP shares the unified DBPath.
// Retained as an unused field so existing callers keep compiling
// during the transition.
IDPDBPath string //nolint:unused // kept for back-compat; ignored by NewHandler.
IDPIssuer string // IDP issuer URL (default: listen address)
// IDPAccessTokenLifespan / IDPRefreshTokenLifespan: see OAuth2*Lifespan above. Zero
// uses the same defaults (1h / 30d).
IDPAccessTokenLifespan time.Duration
IDPRefreshTokenLifespan time.Duration
SessionTTL time.Duration // HTTP session TTL (default: 24h)
HTCondorConfig *config.Config // HTCondor configuration (optional, used for LOCAL_DIR default)
// PingInterval is the cadence of the periodic collector/schedd ping
// that feeds /readyz. Zero or negative disables it, which is what a
// deployment with no local HTCondor credential wants: the ping has
// nothing to authenticate with there and can only fail. There is no
// implicit default — the shipped daemon sets this from
// HTTP_API_PING_INTERVAL (default 1m, 0 to disable).
PingInterval time.Duration
// MCPWatchMaxWait caps how long the MCP watch_jobs tool may block
// in-call before returning (HTTP_API_MCP_WATCH_MAX_WAIT). Keep it
// under the gateway/proxy timeout in front of this daemon: a block
// that outlives it loses the response carrying the watch id. Zero
// uses the mcpserver default.
MCPWatchMaxWait time.Duration
// CreddAddress pins the credd to talk to (HTTP_API_CREDD_ADDRESS),
// overriding discovery. Needed where a schedd does not advertise its
// credd; otherwise the credd is read from the schedd itself.
CreddAddress string
// RequiredCredentials names the OAuth service credentials that must
// exist before a job may be submitted (HTTP_API_REQUIRED_CREDENTIALS).
// Some access points hold every job submitted without them. Each submit
// path creates a placeholder for any that is missing; see
// required_creds.go.
RequiredCredentials []string
// UpstreamRefresh is HTTP_API_UPSTREAM_REFRESH. See Config.
UpstreamRefresh string
// MCPUseSDKTransport serves /mcp with the upstream MCP SDK's transport
// instead of the hand-rolled JSON-RPC handler. HTTP_API_MCP_TRANSPORT.
MCPUseSDKTransport bool
// TokenRetention is HTTP_API_TOKEN_RETENTION. See Config.
TokenRetention string
// MCPMaxRequestDuration is the hard stop on an MCP request that is
// still making progress (HTTP_API_MCP_MAX_REQUEST_DURATION). While a
// request runs, its write deadline is moved forward rather than being
// the server-wide HTTP_API_WRITE_TIMEOUT, so a deliberately waiting
// tool is bounded by this instead. Zero uses
// DefaultMCPMaxRequestDuration.
MCPMaxRequestDuration time.Duration
StreamBufferSize int // Buffer size for streaming queries (default: 100)
StreamWriteTimeout time.Duration // Write timeout for streaming queries (default: 5s)
Token string // Token for daemon authentication (optional)
Credd htcondor.CreddClient // Optional credd client; defaults to in-memory implementation
// Placementd is an optional condor_placementd client. Nil means
// "discover one", and a failed discovery simply leaves the
// placement endpoints disabled. Tests inject a fake here.
Placementd htcondor.PlacementdClient
// LLMAPIKeyFile is the path to a file holding the Anthropic API
// key used by the chat endpoint at /api/v1/chat. Empty disables
// the chat feature. The bytes never live in HTCondor config —
// only this file path does.
LLMAPIKeyFile string
// LLMAPIURL overrides the upstream Anthropic Messages endpoint.
// Use to point at a self-hosted LLM gateway / cache. Empty falls
// back to chat.DefaultAnthropicURL.
LLMAPIURL string
// LLMModel overrides the default Anthropic model id. Empty falls
// back to chat.DefaultAnthropicModel.
LLMModel string
// LLMOperatorInstructionsFile is the path to a file containing
// extra system-prompt rules the operator wants appended to every
// chat turn (e.g. "users in this pool may not request more than
// 64 GiB of memory; suggest fewer if asked"). Empty path = no
// addendum.
//
// File mode is enforced 0600/0400 on load — same rationale as the
// API-key file: the operator may put policy text here that they
// don't want every local user to read. Loaded once at startup; a
// hot-reload would need a server restart.
LLMOperatorInstructionsFile string
// JupyterWorkDir is where the embedded helper binary (materialized
// from package jupyterhelperbin) and per-instance scratch artifacts
// (token files) are staged. Files persist for the lifetime of the
// job since HTCondor reads transfer_input_files at job-startup time.
// Defaults to <os.TempDir>/htcondor-api-jupyter.
JupyterWorkDir string
// JupyterMaxLifetimeSec / JupyterKernelIdleSec bound a JupyterLab
// session; see handlers_jupyter.go. Zero disables that limit.
JupyterMaxLifetimeSec int
JupyterKernelIdleSec int
// TemplateGlobalPath is an optional YAML file with operator-curated
// batch-submission templates. Empty disables. Built-in templates
// always ship.
TemplateGlobalPath string
// TemplateUserStoreDBPath is deprecated; the templates store now
// shares the unified DBPath. Retained so existing callers
// keep compiling; ignored by NewHandler.
TemplateUserStoreDBPath string //nolint:unused // kept for back-compat; ignored by NewHandler.
}
HandlerConfig holds handler configuration
type HistoryListResponse ¶
type HistoryListResponse struct {
Ads []*classad.ClassAd `json:"ads"`
Source string `json:"source,omitempty"`
SourceNote string `json:"source_note,omitempty"`
}
HistoryListResponse represents a history listing response. Source and SourceNote name the backend that answered ("htcondordb" when a synchronized mirror served it, absent for the schedd) so a caller can tell how fresh the records are.
type HoldReasonCount ¶ added in v0.14.1
type HoldReasonCount struct {
Code int64 `json:"code"`
Label string `json:"label"`
Count int `json:"count"`
// Example is one hold reason string with this code, because the code
// says the category and the message says which file or which host.
Example string `json:"example,omitempty"`
}
HoldReasonCount is one row of the hold breakdown.
type IDPProvider ¶
type IDPProvider struct {
// contains filtered or unexported fields
}
IDPProvider manages OAuth2 operations for the built-in IDP
func NewIDPProvider ¶
func NewIDPProvider(opts IDPProviderOptions) (*IDPProvider, error)
NewIDPProvider creates a new IDP provider with SQLite storage. Both AccessTokenLifespan and RefreshTokenLifespan in opts must be > 0; otherwise an error is returned. See OAuth2ProviderOptions for the rationale.
func (*IDPProvider) Close ¶
func (p *IDPProvider) Close() error
Close is now a no-op: the IDP provider does not own the underlying *sql.DB anymore. The Handler that opened the unified app DB is responsible for closing it on shutdown.
func (*IDPProvider) GetProvider ¶
func (p *IDPProvider) GetProvider() fosite.OAuth2Provider
GetProvider returns the underlying fosite OAuth2Provider
func (*IDPProvider) GetStorage ¶
func (p *IDPProvider) GetStorage() *IDPStorage
GetStorage returns the IDP storage
func (*IDPProvider) GetStrategy ¶
func (p *IDPProvider) GetStrategy() *compose.CommonStrategy
GetStrategy returns the OAuth2 strategy
func (*IDPProvider) UpdateIssuer ¶
func (p *IDPProvider) UpdateIssuer(issuer string)
UpdateIssuer updates the issuer URL in the OAuth2 config
type IDPProviderOptions ¶
type IDPProviderOptions struct {
DB *sql.DB
Issuer string
AccessTokenLifespan time.Duration
RefreshTokenLifespan time.Duration
// Sealer envelope-encrypts the IDP's RSA private key + HMAC
// GlobalSecret. See OAuth2ProviderOptions.Sealer.
Sealer *seal.Sealer
}
IDPProviderOptions configures lifespans and other tunables for the IDP provider. DB is the unified application database (see appdb); the provider does not own its lifecycle.
type IDPStorage ¶
type IDPStorage struct {
// contains filtered or unexported fields
}
IDPStorage implements fosite storage interfaces using the unified application database. The IDP's tables (idp_*) live alongside the OAuth2/MCP tables in the same SQLite file managed by appdb.
See OAuth2Storage for the sealer field's role.
func NewIDPStorage ¶
func NewIDPStorage(db *sql.DB) *IDPStorage
NewIDPStorage wraps an already-opened, already-migrated DB. Schema is owned by the appdb migrations; this struct only holds the query helpers. The caller retains DB ownership.
func (*IDPStorage) AuthenticateUser ¶
func (s *IDPStorage) AuthenticateUser(ctx context.Context, username, password string) error
AuthenticateUser verifies username and password
func (*IDPStorage) ClientAssertionJWTValid ¶
func (s *IDPStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error
ClientAssertionJWTValid implements fosite.ClientAssertionJWTValid interface
func (*IDPStorage) CreateAccessTokenSession ¶
func (s *IDPStorage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
CreateAccessTokenSession stores an access token session
func (*IDPStorage) CreateAuthorizeCodeSession ¶
func (s *IDPStorage) CreateAuthorizeCodeSession(ctx context.Context, signature string, request fosite.Requester) error
CreateAuthorizeCodeSession stores an authorization code session
func (*IDPStorage) CreateClient ¶
func (s *IDPStorage) CreateClient(ctx context.Context, client *fosite.DefaultClient) error
CreateClient creates a new OAuth2 client
func (*IDPStorage) CreateOpenIDConnectSession ¶
func (s *IDPStorage) CreateOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) error
CreateOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*IDPStorage) CreatePKCERequestSession ¶
func (s *IDPStorage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
CreatePKCERequestSession stores a PKCE request session
func (*IDPStorage) CreateRefreshTokenSession ¶
func (s *IDPStorage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error
CreateRefreshTokenSession stores a refresh token session
func (*IDPStorage) CreateSession ¶
CreateSession creates a new session for the given username
func (*IDPStorage) CreateUser ¶
func (s *IDPStorage) CreateUser(ctx context.Context, username, password, state string) error
CreateUser creates a new user with hashed password and specified state
func (*IDPStorage) DeleteAccessTokenSession ¶
func (s *IDPStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error
DeleteAccessTokenSession deletes an access token session
func (*IDPStorage) DeleteOpenIDConnectSession ¶
func (s *IDPStorage) DeleteOpenIDConnectSession(ctx context.Context, signature string) error
DeleteOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*IDPStorage) DeletePKCERequestSession ¶
func (s *IDPStorage) DeletePKCERequestSession(ctx context.Context, signature string) error
DeletePKCERequestSession deletes a PKCE request session
func (*IDPStorage) DeleteRefreshTokenSession ¶
func (s *IDPStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
DeleteRefreshTokenSession deletes a refresh token session
func (*IDPStorage) DeleteSession ¶
func (s *IDPStorage) DeleteSession(ctx context.Context, sessionID string) error
DeleteSession deletes a session
func (*IDPStorage) GetAccessTokenSession ¶
func (s *IDPStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetAccessTokenSession retrieves an access token session
func (*IDPStorage) GetAuthorizeCodeSession ¶
func (s *IDPStorage) GetAuthorizeCodeSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetAuthorizeCodeSession retrieves an authorization code session
func (*IDPStorage) GetOpenIDConnectSession ¶
func (s *IDPStorage) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (fosite.Requester, error)
GetOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*IDPStorage) GetPKCERequestSession ¶
func (s *IDPStorage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetPKCERequestSession retrieves a PKCE request session
func (*IDPStorage) GetRefreshTokenSession ¶
func (s *IDPStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetRefreshTokenSession retrieves a refresh token session
func (*IDPStorage) GetSession ¶
GetSession retrieves the username for a given session ID
func (*IDPStorage) GetUserState ¶
GetUserState retrieves the state of a user
func (*IDPStorage) InvalidateAuthorizeCodeSession ¶
func (s *IDPStorage) InvalidateAuthorizeCodeSession(ctx context.Context, signature string) error
InvalidateAuthorizeCodeSession invalidates an authorization code
func (*IDPStorage) LoadHMACSecret ¶
func (s *IDPStorage) LoadHMACSecret(ctx context.Context) ([]byte, error)
LoadHMACSecret loads the HMAC secret. See OAuth2Storage.LoadHMACSecret.
func (*IDPStorage) LoadRSAKey ¶
func (s *IDPStorage) LoadRSAKey(ctx context.Context) (string, error)
LoadRSAKey loads the RSA private key. See OAuth2Storage.LoadRSAKey.
func (*IDPStorage) RevokeAccessToken ¶
func (s *IDPStorage) RevokeAccessToken(ctx context.Context, requestID string) error
RevokeAccessToken revokes an access token
func (*IDPStorage) RevokeAllForSubject ¶ added in v0.13.0
RevokeAllForSubject deactivates every IDP access and refresh token belonging to one subject. See OAuth2Storage.RevokeAllForSubject.
func (*IDPStorage) RevokeRefreshToken ¶
func (s *IDPStorage) RevokeRefreshToken(ctx context.Context, requestID string) error
RevokeRefreshToken revokes a refresh token
func (*IDPStorage) RevokeRefreshTokenMaybeGracePeriod ¶
func (s *IDPStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
RevokeRefreshTokenMaybeGracePeriod implements fosite.TokenRevocationStorage interface
func (*IDPStorage) RotateRefreshToken ¶
RotateRefreshToken revokes the refresh token and its associated access token for the request (required by fosite's RefreshTokenStorage as of v0.49).
func (*IDPStorage) SaveHMACSecret ¶
func (s *IDPStorage) SaveHMACSecret(ctx context.Context, secret []byte) error
SaveHMACSecret stores the HMAC secret. See OAuth2Storage.SaveHMACSecret.
func (*IDPStorage) SaveRSAKey ¶
func (s *IDPStorage) SaveRSAKey(ctx context.Context, privateKeyPEM string) error
SaveRSAKey stores the RSA private key. Mirrors OAuth2Storage's implementation — see SaveRSAKey there for the encryption rationale.
func (*IDPStorage) SetClientAssertionJWT ¶
SetClientAssertionJWT implements fosite.SetClientAssertionJWT interface
func (*IDPStorage) SetSealer ¶
func (s *IDPStorage) SetSealer(sealer *seal.Sealer)
SetSealer enables envelope encryption for the long-lived secret columns (RSA private key, HMAC secret). See OAuth2Storage.SetSealer.
func (*IDPStorage) UserExists ¶
UserExists checks if a user exists
type Impersonation ¶ added in v0.13.0
type Impersonation struct {
// Actor is the authenticated human who armed superuser mode.
Actor string
// Target is the job owner being acted for, derived from the job rather
// than supplied by the caller.
Target string
// Identity is what this server authenticates to the schedd as. Either
// Actor (when they are themselves a queue superuser) or the shared
// fallback.
Identity string
// ActorIsSuperUser records which of those it was. When true the schedd's
// own log names the human; when false the schedd only ever sees the
// shared identity and this server's audit record is the only place the
// actor appears.
ActorIsSuperUser bool
}
Impersonation describes one superuser action: who asked for it, whose job it is, and which identity the server will present to the schedd.
func (Impersonation) Reason ¶ added in v0.13.0
func (i Impersonation) Reason(what string) string
Reason renders the actor and target into a string suitable for a HoldReason / RemoveReason / ReleaseReason.
The schedd appends "(by user <authenticated identity>)" to whatever reason it is given -- see actOnJobs in schedd.cpp -- so when the actor is a queue superuser the job ad ends up naming them twice, from two independent sources. When they are not, the schedd can only append the shared identity, and this prefix is the ONLY record in the job ad of which human acted. That is why the actor goes in the text rather than being left to the schedd.
The result lands in the job ad, so it outlives this server's logs, follows the job into history, and is visible to the job's owner -- who is entitled to know that somebody else touched their job, and which somebody.
type InteractiveCreateTerminalRequest ¶
type InteractiveCreateTerminalRequest struct {
Cpus int `json:"cpus,omitempty"`
MemoryMB int `json:"memory_mb,omitempty"`
DiskMB int `json:"disk_mb,omitempty"`
// GPU fields. Mirrored verbatim into request_gpus and the
// gpus_minimum_* / cuda_version / require_gpus submit lines.
// Gpus == 0 disables the entire GPU section in the submit file.
Gpus int `json:"gpus,omitempty"`
GpusMinimumCapability string `json:"gpus_minimum_capability,omitempty"`
GpusMinimumMemory int `json:"gpus_minimum_memory,omitempty"`
GpusMinimumRuntime string `json:"gpus_minimum_runtime,omitempty"`
CudaVersion string `json:"cuda_version,omitempty"`
RequireGpus string `json:"require_gpus,omitempty"`
// SubmitLines are extra submit commands the user typed in the launch
// form (e.g. "+ProjectName = ...", "environment = ..."). Untrusted:
// validated with interactive.ValidateCallerSubmitLines, which rejects
// anything that would redefine the session (executable, universe,
// queue, ...). Merged before the operator's block so operator policy
// still wins.
SubmitLines string `json:"submit_lines,omitempty"`
}
InteractiveCreateTerminalRequest is the optional JSON body of POST /api/v1/interactive/terminal. All fields are optional; the server fills sensible defaults.
type InteractiveCreateTerminalResponse ¶
type InteractiveCreateTerminalResponse struct {
InstanceID string `json:"instance_id"`
ClusterID int `json:"cluster_id"`
ProcID int `json:"proc_id"`
JobID string `json:"job_id"` // "cluster.proc" — convenience for the SPA
BatchName string `json:"batch_name"`
}
InteractiveCreateTerminalResponse is the JSON returned on success.
type InteractiveTerminalSummary ¶
type InteractiveTerminalSummary struct {
InstanceID string `json:"instance_id"`
JobID string `json:"job_id"`
ClusterID int `json:"cluster_id"`
ProcID int `json:"proc_id"`
BatchName string `json:"batch_name"`
JobStatus int `json:"job_status"`
JobCurrentStartExecutingDate int64 `json:"job_current_start_executing_date,omitempty"`
HoldReasonCode int `json:"hold_reason_code,omitempty"`
HoldReason string `json:"hold_reason,omitempty"`
SubmittedAt string `json:"submitted_at,omitempty"` // RFC3339 from QDate
}
InteractiveTerminalSummary is the SPA-facing shape of one terminal session. Returned by GET /api/v1/interactive/terminal.
JobCurrentStartExecutingDate is the schedd's "executable actually started running" timestamp; combined with JobStatus the SPA's shared status module distinguishes "queued" from "transferring input" from "executing".
type IssueSection ¶ added in v0.19.0
type IssueSection struct {
Kind string `json:"kind"`
Title string `json:"title"`
// Total and Users are over the whole section, so a section header
// can say "4,812 jobs, 11 users" without the reader adding up rows
// -- and so the numbers do not change when the slider does.
Total int `json:"total"`
Users int `json:"users"`
Clusters []issues.Cluster `json:"clusters"`
}
IssueSection is one kind of problem: holds, or run attempts that failed.
type IssueTimings ¶ added in v0.19.0
type IssueTimings struct {
// HoldsMs and RunAttemptsMs are the two reads, with what they
// returned beside them -- a slow read of forty rows and a slow read
// of forty thousand are different problems.
HoldsMs int64 `json:"holds_query_ms"`
Holds int `json:"holds"`
RunAttemptsMs int64 `json:"run_attempts_query_ms"`
RunAttempts int `json:"run_attempts"`
// ClusterMs is the grouping: masking, the parse tree, the merge pass
// and the per-cluster summaries.
ClusterMs int64 `json:"cluster_ms"`
// Cached says the reads were not done for this request. Without it a
// second page load looks fast and hides what the first one cost.
Cached bool `json:"cached"`
// AgeSeconds is how old the cached reads are.
AgeSeconds int64 `json:"age_seconds,omitempty"`
}
IssueTimings splits the cost of one answer.
type IssuesResponse ¶ added in v0.19.0
type IssuesResponse struct {
WindowSeconds int64 `json:"window_seconds"`
ComputedAt int64 `json:"computed_at"`
// Granularity as applied, which may be the clamped form of what was
// asked for.
Granularity float64 `json:"granularity"`
// IncludeEnded says whether run-attempt history was read: without
// it the page describes what is stuck now, with it what has gone
// wrong over the window.
IncludeEnded bool `json:"include_ended"`
// BucketSeconds is how much time one slice of a cluster's timeline
// covers, so a caller can label it without re-deriving the window.
BucketSeconds int64 `json:"bucket_seconds,omitempty"`
Source string `json:"source,omitempty"`
Truncated bool `json:"truncated,omitempty"`
Notes []string `json:"notes,omitempty"`
Sections []IssueSection `json:"sections"`
// Timings is what the answer cost to produce. Returned rather than
// only logged: this page reads two large tables and then does real
// work on what comes back, so "it is slow" has three possible
// answers, and the person who can see the slowness is usually not
// the person who can read the server's log.
Timings *IssueTimings `json:"timings,omitempty"`
}
IssuesResponse is the whole page.
type JobActionFunc ¶
type JobActionFunc func(ctx context.Context, constraint, reason string) (*htcondor.JobActionResults, error)
JobActionFunc is a function that performs a job action (hold, release, etc.)
type JobEditRequest ¶
type JobEditRequest struct {
Attributes map[string]interface{} `json:"attributes"` // Attributes to update
}
JobEditRequest represents a job edit request
type JobListResponse ¶
JobListResponse represents a job listing response
type JobLogResponse ¶
type JobLogResponse struct {
JobID string `json:"jobId"`
Filename string `json:"filename"`
Truncated bool `json:"truncated"`
Events []userlog.Event `json:"events"`
}
JobLogResponse is the JSON shape returned by GET /api/v1/jobs/{id}/log. It mirrors the stdout/stderr endpoints — explicit fetch, no streaming.
type JobSubmitRequest ¶
type JobSubmitRequest struct {
SubmitFile string `json:"submit_file"` // Submit file content
}
JobSubmitRequest represents a job submission request
type JobSubmitResponse ¶
type JobSubmitResponse struct {
ClusterID int `json:"cluster_id"`
JobIDs []string `json:"job_ids"` // Array of "cluster.proc" strings
}
JobSubmitResponse represents a job submission response
type JupyterCreateRequest ¶
type JupyterCreateRequest struct {
// Image is the Docker image to launch. Default
// quay.io/jupyter/scipy-notebook:latest.
Image string `json:"image"`
// Cpus is the requested core count. Default 2.
Cpus int `json:"cpus"`
// MemoryMB is the requested RAM in mebibytes. Default 4096.
MemoryMB int `json:"memory_mb"`
// DiskMB is the requested scratch disk in mebibytes. Default 4096.
DiskMB int `json:"disk_mb"`
// GPU fields. Mirrored verbatim into request_gpus and the
// gpus_minimum_* / cuda_version / require_gpus submit lines.
// Gpus == 0 disables the entire GPU section in the submit file.
Gpus int `json:"gpus,omitempty"`
GpusMinimumCapability string `json:"gpus_minimum_capability,omitempty"`
GpusMinimumMemory int `json:"gpus_minimum_memory,omitempty"`
GpusMinimumRuntime string `json:"gpus_minimum_runtime,omitempty"`
CudaVersion string `json:"cuda_version,omitempty"`
RequireGpus string `json:"require_gpus,omitempty"`
// SubmitLines are extra submit commands the user typed in the launch
// form. Untrusted: validated with interactive.ValidateCallerSubmitLines,
// which rejects anything that would redefine the job (executable,
// universe, container_image, queue, ...). Merged before the operator's
// extras so operator policy still wins.
SubmitLines string `json:"submit_lines,omitempty"`
}
JupyterCreateRequest is the optional JSON body of POST /jupyter/instances. All fields have sensible defaults so a bare {} is a valid request.
type JupyterCreateResponse ¶
type JupyterCreateResponse struct {
InstanceID string `json:"instance_id"`
ClusterID string `json:"cluster_id"`
// ProxyPath is where the browser should eventually point its iframe
// (only useful once the helper has connected back; the SSE stream
// from /events tells you when).
ProxyPath string `json:"proxy_path"`
}
JupyterCreateResponse is the JSON returned by POST /jupyter/instances.
type JupyterInstanceSummary ¶
type JupyterInstanceSummary struct {
InstanceID string `json:"instance_id"`
ClusterID string `json:"cluster_id,omitempty"`
Image string `json:"image,omitempty"`
Owner string `json:"owner"`
CreatedAt string `json:"created_at"`
Connected bool `json:"connected"` // helper has dialed back
ProxyPath string `json:"proxy_path"`
EventsPath string `json:"events_path"`
JobStatus int `json:"job_status,omitempty"`
JobCurrentStartExecutingDate int64 `json:"job_current_start_executing_date,omitempty"`
HoldReasonCode int `json:"hold_reason_code,omitempty"`
HoldReason string `json:"hold_reason,omitempty"`
}
JupyterInstanceSummary is the SPA-facing shape returned by both GET /api/v1/jupyter/instances (list) and GET /api/v1/jupyter/instances/{id} (single). The proxy_path is what the iframe should mount; the events_path drives the SSE stream.
The job_* fields are populated from a single bulk schedd query (handleJupyterListInstances) so the list view can run the same status-interpretation logic the detail page uses, without a round-trip per row. Empty when the schedd query failed or the cluster is gone — the SPA falls back to a "loading"/"connected only" view in that case.
type LoginRateLimiter ¶
type LoginRateLimiter struct {
// contains filtered or unexported fields
}
LoginRateLimiter manages rate limiting for login attempts per IP address
func NewLoginRateLimiter ¶
func NewLoginRateLimiter(r rate.Limit, b int) *LoginRateLimiter
NewLoginRateLimiter creates a new login rate limiter rate: maximum requests per second per IP burst: maximum burst size per IP
func (*LoginRateLimiter) Allow ¶
func (l *LoginRateLimiter) Allow(ip string) bool
Allow checks if a login attempt from the given IP is allowed
type OAuth2Provider ¶
type OAuth2Provider struct {
// contains filtered or unexported fields
}
OAuth2Provider manages OAuth2 operations
func NewOAuth2Provider ¶
func NewOAuth2Provider(opts OAuth2ProviderOptions) (*OAuth2Provider, error)
NewOAuth2Provider creates a new OAuth2 provider with SQLite storage. Both AccessTokenLifespan and RefreshTokenLifespan in opts must be > 0; otherwise an error is returned. This is intentional: silent fallback to fosite's defaults (1h access, 30d refresh) has bitten downstream projects when callers forget to pass them through, so callers must opt in explicitly.
func (*OAuth2Provider) AuthenticateClient ¶ added in v0.14.0
func (p *OAuth2Provider) AuthenticateClient(ctx context.Context, r *http.Request, form url.Values) (fosite.Client, error)
AuthenticateClient authenticates the client on a token request (client_secret_basic / client_secret_post), for custom grant flows that bypass fosite's NewAccessRequest pipeline -- notably RFC 8693 token exchange. The concrete provider from compose.Compose is *fosite.Fosite, which exposes the same client-authentication strategy the standard token endpoint uses.
func (*OAuth2Provider) Close ¶
func (p *OAuth2Provider) Close() error
Close is now a no-op: the OAuth2 provider does not own the underlying *sql.DB anymore. The Handler that opened the unified app DB is responsible for closing it on shutdown. Method retained so callers that defer p.Close() during refactors don't break.
func (*OAuth2Provider) GetProvider ¶
func (p *OAuth2Provider) GetProvider() fosite.OAuth2Provider
GetProvider returns the underlying fosite OAuth2Provider
func (*OAuth2Provider) GetStorage ¶
func (p *OAuth2Provider) GetStorage() *OAuth2Storage
GetStorage returns the OAuth2 storage
func (*OAuth2Provider) GetStrategy ¶
func (p *OAuth2Provider) GetStrategy() *compose.CommonStrategy
GetStrategy returns the OAuth2 strategy
func (*OAuth2Provider) IntrospectAccessToken ¶ added in v0.14.0
func (p *OAuth2Provider) IntrospectAccessToken(ctx context.Context, token string) (fosite.AccessRequester, error)
IntrospectAccessToken validates one of our access tokens and returns the requester behind it (subject via GetSession, and the granted scopes), or an error if the token is unknown/expired/revoked. Used by token exchange to bind a subject_token to its authorization. Unlike IntrospectToken it keeps the requester, which is where the granted scopes live.
func (*OAuth2Provider) IntrospectToken ¶
IntrospectToken validates an access token and returns the session
func (*OAuth2Provider) UpdateIssuer ¶
func (p *OAuth2Provider) UpdateIssuer(issuer string)
UpdateIssuer updates the issuer URL in the configuration This is useful when using port 0 and getting the actual port after server start
type OAuth2ProviderOptions ¶
type OAuth2ProviderOptions struct {
DB *sql.DB
Issuer string
AccessTokenLifespan time.Duration
RefreshTokenLifespan time.Duration
// Sealer envelope-encrypts long-lived secrets in the DB (the
// issuer's RSA private key, fosite's HMAC GlobalSecret). When
// non-nil, the storage adapter pulls/pushes ciphertext + wrapped
// DEK on the corresponding load/save calls. Nil = plaintext.
Sealer *seal.Sealer
// CIMDEnabled turns on Client ID Metadata Document resolution: an https://
// client_id is fetched and treated as a public client (see oauth2_cimd.go).
CIMDEnabled bool
// CIMDAllowedHosts optionally restricts which hosts a CIMD client_id may
// point at; empty means any host (the SSRF guards still apply).
CIMDAllowedHosts []string
}
OAuth2ProviderOptions configures lifespans and other tunables for the OAuth2 provider. Lifespans must be > 0; callers are expected to validate or default before constructing. DB is the unified application database (see appdb); the provider does not own its lifecycle.
type OAuth2StateEntry ¶
type OAuth2StateEntry struct {
AuthorizeRequest fosite.AuthorizeRequester
Timestamp time.Time
OriginalURL string // Original URL to redirect back to after authentication
Username string // Authenticated username for consent flow
Groups []string // User groups for scope filtering in consent flow
}
OAuth2StateEntry represents a stored OAuth2 authorization state
type OAuth2StateStore ¶
type OAuth2StateStore struct {
// contains filtered or unexported fields
}
OAuth2StateStore manages OAuth2 state parameters for the authorization flow
func NewOAuth2StateStore ¶
func NewOAuth2StateStore() *OAuth2StateStore
NewOAuth2StateStore creates a new OAuth2 state store Call Start() to begin the cleanup goroutine
func (*OAuth2StateStore) GenerateState ¶
func (s *OAuth2StateStore) GenerateState() (string, error)
GenerateState generates a secure random state parameter
func (*OAuth2StateStore) Get ¶
func (s *OAuth2StateStore) Get(state string) (fosite.AuthorizeRequester, bool)
Get retrieves and removes an authorize request for the given state
func (*OAuth2StateStore) GetWithURL ¶
func (s *OAuth2StateStore) GetWithURL(state string) (fosite.AuthorizeRequester, string, bool)
GetWithURL retrieves and removes an authorize request for the given state along with the original URL
func (*OAuth2StateStore) GetWithUsername ¶
func (s *OAuth2StateStore) GetWithUsername(state string) (fosite.AuthorizeRequester, string, []string, bool)
GetWithUsername retrieves an authorize request for the given state along with username and groups (without removing)
func (*OAuth2StateStore) Remove ¶
func (s *OAuth2StateStore) Remove(state string)
Remove removes an entry for the given state
func (*OAuth2StateStore) Start ¶
func (s *OAuth2StateStore) Start(ctx context.Context)
Start begins the cleanup goroutine
func (*OAuth2StateStore) Store ¶
func (s *OAuth2StateStore) Store(state string, ar fosite.AuthorizeRequester)
Store stores an authorize request with the given state
func (*OAuth2StateStore) StoreWithURL ¶
func (s *OAuth2StateStore) StoreWithURL(state string, ar fosite.AuthorizeRequester, originalURL string)
StoreWithURL stores an authorize request with the given state and original URL
func (*OAuth2StateStore) StoreWithUsername ¶
func (s *OAuth2StateStore) StoreWithUsername(state string, ar fosite.AuthorizeRequester, originalURL, username string, groups ...[]string)
StoreWithUsername stores an authorize request with the given state, original URL, and username
func (*OAuth2StateStore) Wait ¶
func (s *OAuth2StateStore) Wait()
Wait waits for the cleanup goroutine to finish
type OAuth2Storage ¶
type OAuth2Storage struct {
// contains filtered or unexported fields
}
OAuth2Storage implements fosite storage interfaces using the unified application database. The schema is owned by appdb's migrations — this struct is purely a thin set of query helpers around an already- migrated *sql.DB.
The optional `sealer` field is the application's envelope-encryption gate. When non-nil, SaveRSAKey / SaveHMACSecret store ciphertext + wrapped DEK; LoadRSAKey / LoadHMACSecret transparently decrypt rows whose DEK column is populated. When nil, the storage falls back to the pre-encryption plaintext behavior — back-compat for deployments that haven't configured a KEK yet.
func NewOAuth2Storage ¶
func NewOAuth2Storage(db *sql.DB) *OAuth2Storage
NewOAuth2Storage wraps an already-opened DB in the OAuth2 storage helpers. Schema creation is no longer this struct's responsibility — see httpserver/appdb. The caller retains ownership of the DB (don't call Close() here on shutdown).
The returned storage starts in plaintext mode; call SetSealer if a KEK has been loaded.
func (*OAuth2Storage) ApproveDeviceCodeSession ¶
func (s *OAuth2Storage) ApproveDeviceCodeSession(ctx context.Context, userCode string, subject string, session fosite.Session) error
ApproveDeviceCodeSession approves a device code (user authorized the device)
func (*OAuth2Storage) ApproveDeviceCodeSessionWithScopes ¶
func (s *OAuth2Storage) ApproveDeviceCodeSessionWithScopes(ctx context.Context, userCode string, subject string, session fosite.Session, grantedScopes []string) error
ApproveDeviceCodeSessionWithScopes is like ApproveDeviceCodeSession but also overrides the device code's granted_scopes column with the supplied subset. Use this when the consent UI showed the user per-scope checkboxes and the user declined some — the resulting access token must reflect the user-approved intersection, not the originally-requested set.
Pass nil grantedScopes to leave the existing granted_scopes untouched (equivalent to ApproveDeviceCodeSession). Pass an empty (non-nil) slice to record "user explicitly approved zero scopes" — useful as a sentinel; fosite will refuse to mint a token for a no-scope grant, but we want the audit log to show the user's choice.
func (*OAuth2Storage) ClientAssertionJWTValid ¶
func (s *OAuth2Storage) ClientAssertionJWTValid(ctx context.Context, jti string) error
ClientAssertionJWTValid implements fosite.ClientAssertionJWTValid interface This checks if a JWT ID (JTI) has already been used to prevent replay attacks
func (*OAuth2Storage) CreateAccessTokenSession ¶
func (s *OAuth2Storage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
CreateAccessTokenSession stores an access token session
func (*OAuth2Storage) CreateAuthorizeCodeSession ¶
func (s *OAuth2Storage) CreateAuthorizeCodeSession(ctx context.Context, signature string, request fosite.Requester) error
CreateAuthorizeCodeSession stores an authorization code session
func (*OAuth2Storage) CreateClient ¶
func (s *OAuth2Storage) CreateClient(ctx context.Context, client *fosite.DefaultClient) error
CreateClient creates a new OAuth2 client
func (*OAuth2Storage) CreateDeviceCodeSession ¶
func (s *OAuth2Storage) CreateDeviceCodeSession(ctx context.Context, deviceCode string, userCode string, request fosite.Requester, expiresAt time.Time) error
CreateDeviceCodeSession creates a new device code session
func (*OAuth2Storage) CreateOpenIDConnectSession ¶
func (s *OAuth2Storage) CreateOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) error
CreateOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*OAuth2Storage) CreatePKCERequestSession ¶
func (s *OAuth2Storage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
CreatePKCERequestSession stores a PKCE request session
func (*OAuth2Storage) CreateRefreshTokenSession ¶
func (s *OAuth2Storage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error
CreateRefreshTokenSession stores a refresh token session. As of fosite v0.49 the signature carries the associated access token signature; we key sessions off the refresh signature and revoke by request ID, so it is not stored.
func (*OAuth2Storage) DeleteAccessTokenSession ¶
func (s *OAuth2Storage) DeleteAccessTokenSession(ctx context.Context, signature string) error
DeleteAccessTokenSession deletes an access token session
func (*OAuth2Storage) DeleteOpenIDConnectSession ¶
func (s *OAuth2Storage) DeleteOpenIDConnectSession(ctx context.Context, signature string) error
DeleteOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*OAuth2Storage) DeletePKCERequestSession ¶
func (s *OAuth2Storage) DeletePKCERequestSession(ctx context.Context, signature string) error
DeletePKCERequestSession deletes a PKCE request session
func (*OAuth2Storage) DeleteRefreshTokenSession ¶
func (s *OAuth2Storage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
DeleteRefreshTokenSession deletes a refresh token session
func (*OAuth2Storage) DenyDeviceCodeSession ¶
func (s *OAuth2Storage) DenyDeviceCodeSession(ctx context.Context, userCode string) error
DenyDeviceCodeSession denies a device code (user rejected the device)
func (*OAuth2Storage) EnsureGrantAuthorizedScopes ¶ added in v0.19.0
func (s *OAuth2Storage) EnsureGrantAuthorizedScopes(ctx context.Context, requestID string, scopes []string) error
EnsureGrantAuthorizedScopes records what a grant was authorized with, for grants that predate its being captured at consent.
Every grant in existence when that started being recorded has none, and without this the admin page reads the scopes in force as the whole authorization. Switching one off then shrinks the set it would restore from, so the scope cannot be put back -- the one-way door the toggle exists to remove, for exactly the grants an operator already had.
Called with the set in force BEFORE a change, which for a grant nobody has touched is what it was authorized with. A no-op once a value is present, so a later narrowing cannot overwrite the original.
The session is rewritten through a generic map rather than the Session type: decoding into Session and re-encoding would drop any field this build does not know about, and a token session carries the OIDC claims.
func (*OAuth2Storage) FindGrantBySignaturePrefix ¶ added in v0.17.0
func (s *OAuth2Storage) FindGrantBySignaturePrefix(ctx context.Context, kind, prefix string) (GrantRef, error)
FindGrantBySignaturePrefix resolves the fingerprint shown in the admin token listing back to the grant behind it.
kind selects the table, because the listing shows access and refresh tokens together and their signatures live in different ones.
func (*OAuth2Storage) GetAccessTokenSession ¶
func (s *OAuth2Storage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetAccessTokenSession retrieves an access token session
func (*OAuth2Storage) GetAuthorizeCodeSession ¶
func (s *OAuth2Storage) GetAuthorizeCodeSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetAuthorizeCodeSession retrieves an authorization code session
func (*OAuth2Storage) GetDB ¶
func (s *OAuth2Storage) GetDB() *sql.DB
GetDB returns the underlying database connection. Kept on the struct because tests and the SessionStore wiring still reach for it.
func (*OAuth2Storage) GetDeviceCodeSession ¶
func (s *OAuth2Storage) GetDeviceCodeSession(ctx context.Context, deviceCode string, session fosite.Session) (fosite.Requester, error)
GetDeviceCodeSession retrieves a device code session by device code
func (*OAuth2Storage) GetDeviceCodeSessionByUserCode ¶
func (s *OAuth2Storage) GetDeviceCodeSessionByUserCode(ctx context.Context, userCode string) (string, fosite.Requester, error)
GetDeviceCodeSessionByUserCode retrieves a device code session by user code
func (*OAuth2Storage) GetOpenIDConnectSession ¶
func (s *OAuth2Storage) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (fosite.Requester, error)
GetOpenIDConnectSession implements openid.OpenIDConnectRequestStorage interface
func (*OAuth2Storage) GetPKCERequestSession ¶
func (s *OAuth2Storage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetPKCERequestSession retrieves a PKCE request session
func (*OAuth2Storage) GetRefreshTokenSession ¶
func (s *OAuth2Storage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)
GetRefreshTokenSession retrieves a refresh token session
func (*OAuth2Storage) GrantAuthorizedScopes ¶ added in v0.18.0
func (s *OAuth2Storage) GrantAuthorizedScopes(ctx context.Context, requestID string) ([]string, error)
GrantAuthorizedScopes reads what a grant's authorization ENDED with: the set an operator may restore it to.
Read from the stored session rather than a column of its own, because that session is what fosite carries forward across every refresh -- a column would have to be re-derived on each new token row, and the set it has to preserve is the one from the ORIGINAL authorization, not from whatever the grant has been narrowed to since.
A grant issued before this was recorded has none. Its current scopes are then the only defensible bound: the alternative is inventing an authorization nobody made.
func (*OAuth2Storage) GrantScopes ¶ added in v0.18.0
GrantScopes reads the scopes currently granted under one grant.
Read from the access token where there is one, falling back to the refresh token: the two carry the same granted set by construction, and a grant whose access token has expired still has a refresh token an operator may want to narrow.
func (*OAuth2Storage) InvalidateAuthorizeCodeSession ¶
func (s *OAuth2Storage) InvalidateAuthorizeCodeSession(ctx context.Context, signature string) error
InvalidateAuthorizeCodeSession invalidates an authorization code
func (*OAuth2Storage) InvalidateDeviceCodeSession ¶
func (s *OAuth2Storage) InvalidateDeviceCodeSession(ctx context.Context, deviceCode string) error
InvalidateDeviceCodeSession invalidates a device code after it's been used
func (*OAuth2Storage) LoadHMACSecret ¶
func (s *OAuth2Storage) LoadHMACSecret(ctx context.Context) ([]byte, error)
LoadHMACSecret loads the HMAC secret. See LoadRSAKey for the encryption-vs-plaintext branching.
func (*OAuth2Storage) LoadRSAKey ¶
func (s *OAuth2Storage) LoadRSAKey(ctx context.Context) (string, error)
LoadRSAKey loads the RSA private key. Falls back to plaintext when the row's DEK column is NULL — that's the pre-encryption format and the format used when no KEK is configured. When a DEK is present but no sealer is configured (KEK was removed without rotating data), returns an explicit error rather than handing back ciphertext or silently regenerating the key.
func (*OAuth2Storage) RevokeAccessToken ¶
func (s *OAuth2Storage) RevokeAccessToken(ctx context.Context, requestID string) error
RevokeAccessToken revokes an access token
func (*OAuth2Storage) RevokeAllForSubject ¶ added in v0.13.0
RevokeAllForSubject deactivates every access and refresh token belonging to one subject, across all clients.
This is the operator's answer to "this person is gone, cut them off now". The refresh-time oracles handle the steady state, but they only fire when the user's client next shows up, and only when an oracle can see the removal at all; an admin needs a way to act immediately and unconditionally.
It returns the number of token rows deactivated. Rows are marked inactive rather than deleted so the admin token listing can still show what was revoked.
func (*OAuth2Storage) RevokeGrant ¶ added in v0.17.0
RevokeGrant deactivates every token issued under one grant -- the access token and the refresh token that came with it.
Revoking only the access token would be theatre: a client holding the refresh token mints a new one within minutes, which is exactly the case an operator reaches for this to stop. Rows are deactivated rather than deleted, matching RevokeAllForSubject, so the listing can still show what was revoked.
func (*OAuth2Storage) RevokeRefreshToken ¶
func (s *OAuth2Storage) RevokeRefreshToken(ctx context.Context, requestID string) error
RevokeRefreshToken revokes a refresh token
func (*OAuth2Storage) RevokeRefreshTokenMaybeGracePeriod ¶
func (s *OAuth2Storage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
RevokeRefreshTokenMaybeGracePeriod implements fosite.TokenRevocationStorage interface This handles refresh token revocation. The signature parameter allows for grace period implementation but for simplicity we immediately revoke the token by request ID
func (*OAuth2Storage) RotateRefreshToken ¶
RotateRefreshToken revokes the refresh token and its associated access token for the request, mirroring fosite's reference rotation semantics (required by the RefreshTokenStorage interface as of v0.49). The refresh token signature is unused because revocation is keyed by request ID.
func (*OAuth2Storage) SaveHMACSecret ¶
func (s *OAuth2Storage) SaveHMACSecret(ctx context.Context, secret []byte) error
SaveHMACSecret stores the HMAC secret. See SaveRSAKey for the encryption-vs-plaintext branching.
func (*OAuth2Storage) SaveRSAKey ¶
func (s *OAuth2Storage) SaveRSAKey(ctx context.Context, privateKeyPEM string) error
SaveRSAKey stores the RSA private key. When a sealer is set the PEM bytes are encrypted under a fresh per-row DEK (itself wrapped by the DB-instance KEK); when no sealer is configured the PEM is written verbatim — same on-disk shape as the pre-KEK schema, kept for back-compat with deployments that haven't enabled encryption.
func (*OAuth2Storage) SetClientAssertionJWT ¶
SetClientAssertionJWT implements fosite.SetClientAssertionJWT interface This stores the JTI (JWT ID) with expiration to prevent replay attacks
func (*OAuth2Storage) SetGrantScopes ¶ added in v0.18.0
func (s *OAuth2Storage) SetGrantScopes(ctx context.Context, requestID string, scopes []string) (int64, error)
SetGrantScopes rewrites the granted scopes of every token issued under one grant.
Applied to the whole grant for the same reason RevokeGrant is: narrowing only the access token would be undone at the next refresh, minutes later, by a client that still holds a refresh token carrying the old set. The operator reaching for this wants the narrowing to stick.
Only granted_scopes is rewritten. The `scopes` column records what was REQUESTED, which is a fact about a past request and not this server's to revise -- and reauthorizeRefreshGrant re-derives the allowed set from the granted one, so that is the column that decides what the token can do.
func (*OAuth2Storage) SetSealer ¶
func (s *OAuth2Storage) SetSealer(sealer *seal.Sealer)
SetSealer enables envelope encryption for the long-lived secret columns (RSA private key, HMAC secret). Calling with nil restores plaintext mode. Callers should set this once at startup, before SaveRSAKey / SaveHMACSecret have a chance to fire — the startup-backfill path in NewHandler does exactly that.
func (*OAuth2Storage) UpdateDeviceCodePolling ¶
func (s *OAuth2Storage) UpdateDeviceCodePolling(ctx context.Context, deviceCode string) error
UpdateDeviceCodePolling updates the last polled timestamp for rate limiting
type PeekResponse ¶
type PeekResponse struct {
Stdout *PeekedStreamResponse `json:"stdout,omitempty"`
Stderr *PeekedStreamResponse `json:"stderr,omitempty"`
}
PeekResponse mirrors htcondor.PeekResult on the wire. Fields that weren't requested (or that the starter elected not to return) are omitted entirely so the SPA can detect "stream wasn't transferred" without inferring from a zero-length string.
type PeekedStreamResponse ¶
PeekedStreamResponse is the JSON shape returned for one of the requested streams. `bytes` is the raw text the starter sent (the caller is responsible for handling NUL/binary content if it shows up — stdout/stderr are nearly always UTF-8). `offset` is the absolute file offset *after* this read; pass it back as `stdout_offset` / `stderr_offset` on the next call to follow.
type PingResponse ¶
type PingResponse struct {
Daemon string `json:"daemon"` // "collector" or "schedd"
AuthMethod string `json:"auth_method"` // Authentication method used
User string `json:"user"` // Authenticated username
SessionID string `json:"session_id"` // Session identifier
ValidCommands string `json:"valid_commands"` // Commands authorized
Encryption bool `json:"encryption"` // Whether encryption is enabled
Authentication bool `json:"authentication"` // Whether authentication is enabled
Authorized bool `json:"authorized,omitempty"` // Whether authorized for requested permission (if permission checked)
Permission string `json:"permission,omitempty"` // Permission level checked (if any)
}
PingResponse represents a ping response for a daemon
type ReauthDecision ¶ added in v0.13.0
type ReauthDecision struct {
// Status is the verdict on the user as a whole.
Status UserStatus
// Reason is a short operator-facing explanation, surfaced in logs and
// (for revocations) in the OAuth2 error description. HTCondor's
// per-user records carry a DisableReason string that lands here.
Reason string
// DeniedScopes are scopes the oracle says this user may no longer
// hold, even when Status is Active. They are removed from the
// refreshed grant rather than failing it, so a user who loses write
// access keeps working read-only instead of being logged out.
DeniedScopes []string
}
ReauthDecision is what a RevocationOracle reports about one user.
type RecentJob ¶ added in v0.14.1
type RecentJob struct {
ClusterID int64 `json:"cluster_id"`
ProcID int64 `json:"proc_id"`
Owner string `json:"owner,omitempty"`
// At is when the event this list is about happened, unix seconds.
At int64 `json:"at"`
// Detail is the one fact worth showing beside it: a hold reason, the
// executable, the host it started on.
Detail string `json:"detail,omitempty"`
// Archived says this row came from the history archive rather than
// the live queue, which decides where a click on it should go. The
// queue destroys a finished job within seconds, so most completions
// shown here no longer have a job page -- linking them all to one
// sent people to "not found".
Archived bool `json:"archived,omitempty"`
}
RecentJob is one entry in a recent-activity list.
type RevocationOracle ¶ added in v0.13.0
type RevocationOracle interface {
// Name identifies the oracle in log lines.
Name() string
// Check reports on username, which is the grant's subject. scopes is
// the set currently granted, so an oracle can skip work for scopes
// nobody holds.
Check(ctx context.Context, username string, scopes []string) (ReauthDecision, error)
}
RevocationOracle answers "is this user still entitled to what they were granted?" at refresh time.
Implementations must fail OPEN: a backend that is unreachable, slow, or simply has no record of the user returns UserStatusUnknown, never UserStatusRevoked. A refresh endpoint that hard-denies whenever a dependency hiccups is an outage amplifier, and the absolute grant lifetime cap is what bounds exposure when every oracle is silent.
type ScheddACLOracle ¶ added in v0.13.0
type ScheddACLOracle struct {
Schedd func() *htcondor.Schedd
UIDDomain string
Logger *logging.Logger
// MintToken produces an HTCondor IDTOKEN asserting username, used as
// the probe credential. The scopes argument is passed through to the
// handler's minter; Check passes nil so the probe token carries no
// limit_authz narrowing (see Check for why).
MintToken func(username string, scopes []string) (string, error)
}
ScheddACLOracle strips scopes whose HTCondor authorization level the schedd would refuse for this user, by running a DC_SEC_QUERY probe — the same question condor_ping asks.
It is the weaker of the two oracles and is deliberately scoped to narrowing rather than revoking. Two reasons:
- It cannot see identity at all. The MCP server mints the IDTOKEN it probes with, so the schedd is being asked "do your ACLs admit this name", not "does this person still exist". A deleted user whose name still matches ALLOW_WRITE passes.
- ALLOW_WRITE is `*@uid_domain` in many pools, in which case the probe tells you nothing about any individual.
What it does catch is a pool that genuinely enumerates users in its ACLs, where removing someone from ALLOW_WRITE should stop their write access without waiting for the grant's lifetime cap.
func (*ScheddACLOracle) Check ¶ added in v0.13.0
func (o *ScheddACLOracle) Check(ctx context.Context, username string, scopes []string) (ReauthDecision, error)
Check implements RevocationOracle.
It probes only the levels the user actually holds: a grant with no write scope never asks about WRITE. The verdict is always Active — this oracle answers a question about permissions, not about the person — with denied scopes listed for whatever the schedd refuses.
func (*ScheddACLOracle) Name ¶ added in v0.13.0
func (o *ScheddACLOracle) Name() string
Name implements RevocationOracle.
type Server ¶
type Server struct {
*Handler // Embedded handler for business logic
// contains filtered or unexported fields
}
Server represents the HTTP API server
func (*Server) GetAddr ¶
GetAddr returns the actual listening address of the server. Returns empty string if the server hasn't started yet.
func (*Server) ServeAdditionalListener ¶ added in v0.14.1
ServeAdditionalListener serves the already-running server on a second listener.
Under condor_master the daemon is handed a socket -- a shared-port endpoint or a pre-created command socket -- and that is what carries CEDAR commands. An operator who also wants the web UI on a port of their choosing, 443 being the one people ask for, needs both at once: the master's socket for the pool, and a directly dialable port for browsers. The master cannot be told to hand down 443, so this process has to bind it itself.
The handler is started by ServeListenerWithCert and must not be started again -- doing so would register every route a second time and start a second copy of each background goroutine. This only feeds another listener into the same server, so call it after the primary one is serving.
func (*Server) ServeListener ¶
ServeListener runs the API server on a caller-supplied net.Listener. scheme controls which protocol the request URLs are advertised under ("http" or "https") — use "https" if the caller has configured httpServer.TLSConfig, "http" otherwise.
This is the entry point used when condor_master spawns us as a managed daemon and we accept forwarded connections from condor_shared_port via a sharedport.Listener instead of binding our own TCP port. The handler bootstrap (issuer URL, OAuth2 setup) is the same as Start/StartTLS; the only difference is the kind of listener we hand to http.Server.Serve.
func (*Server) ServeListenerWithCert ¶
ServeListenerWithCert is the listener-injecting analog of Start/StartTLS used by the daemon framework, which supplies the (shared-port or TCP) listener: it runs the handler then serves on ln, terminating TLS from certFile/keyFile when both are set (https) or speaking plain HTTP otherwise. It returns http.ErrServerClosed after Shutdown, like the standard library.
func (*Server) ServeMCPListener ¶ added in v0.14.1
ServeMCPListener serves only the MCP surface on its own listener.
A second http.Server rather than another listener on the first: the two ports deliberately expose different things, and that difference is the whole point of asking for a separate port. The handler is the same one, fronted by a filter.
The handler is started by ServeListenerWithCert and must not be started again, so call this after the primary listener is serving. Shutdown closes both.
type Session ¶ added in v0.13.0
type Session struct {
*openid.DefaultSession
// Groups is the group list asserted by the upstream IDP (or the
// browser session) at the time consent was granted.
Groups []string `json:"groups,omitempty"`
// AuthTime is when the user authenticated and consented, in UTC.
// It is deliberately NOT refreshed when the grant is refreshed.
AuthTime time.Time `json:"authTime,omitempty"`
// AuthorizedScopes is what this authorization ENDED with: the set the
// policy allowed and the user did not untick, recorded once at
// consent and carried unchanged across every refresh.
//
// It is the bound on what an operator may re-enable from the admin
// page. GrantedScope is the set in force now, which an operator may
// have narrowed; this is the set that narrowing started from, so
// putting one back is restoring something this user already agreed
// to rather than granting something new. A scope the user unticked at
// consent never appears here, so no operator can undo that choice.
AuthorizedScopes []string `json:"authorizedScopes,omitempty"`
// Actor names the client that obtained this token by RFC 8693 token
// exchange on the subject's behalf (delegation): the token acts AS Subject
// but was minted FOR this actor. Empty for tokens obtained directly. It is
// the `act.sub` an exchanged token records, and is surfaced for audit and
// on the HTCondor IDTOKEN minted downstream.
Actor string `json:"actor,omitempty"`
}
Session is the fosite session persisted for every grant this server issues, for both the MCP OAuth2 provider and the built-in IDP.
It exists to carry the *inputs* of the authorization decision alongside its output, so that a refresh grant can recompute the decision instead of replaying it. fosite's refresh pipeline rebuilds a request by cloning the stored session and re-granting whatever scopes the original grant carried (see handler/oauth2/flow_refresh.go), so anything the refresh path needs has to survive that round trip. Two fields do that work:
- Groups: the IDP-asserted group memberships that produced the granted scopes at consent time. They are read once from the userinfo endpoint and otherwise dropped on the floor, so without persisting them here the refresh path cannot re-run getScopesForGroups at all. Note this is a snapshot, not a live reading — see reauthorizeRefreshGrant for what that does and does not catch.
- AuthTime: when the human actually authenticated and consented. Every refresh resets the refresh token's own expiry, so AuthTime is the only fixed point from which an absolute cap on the grant can be measured.
Both are advisory inputs to reauthorizeRefreshGrant; nothing else reads them, and a session that predates this type (deserialized from a row written by an older build) simply has them zero-valued. See reauthorizeRefreshGrant for how that case is handled.
func DefaultIDPSession ¶
DefaultIDPSession creates a default OpenID Connect session for the IDP. See DefaultOpenIDConnectSession for why this returns *Session.
func DefaultOpenIDConnectSession ¶
DefaultOpenIDConnectSession creates a default OpenID Connect session.
The concrete type is *Session, not *openid.DefaultSession: grants issued by this server must carry the group list and auth time that reauthorizeRefreshGrant re-checks when the grant is later refreshed.
func (*Session) Clone ¶ added in v0.13.0
Clone deep-copies the session, including the fields declared above.
This override is load-bearing; do not delete it. openid.DefaultSession has its own Clone that reflection-copies its receiver, and if that method were left to promote through the embedded pointer it would return a bare *openid.DefaultSession and silently drop Groups and AuthTime. fosite's refresh handler rebuilds every refreshed request via originalRequest.GetSession().Clone(), so a promoted Clone would erase exactly the data the refresh path exists to consult — and erase it quietly, because the result still satisfies fosite.Session and a missing group list is indistinguishable from "this user has no groups".
TestSessionClonedeepPreservesFields guards this.
func (*Session) WithAuthorizedScopes ¶ added in v0.18.0
WithAuthorizedScopes records what this authorization ended with, which is the bound on what an operator may later re-enable. See AuthorizedScopes.
Called at every site that grants scopes, so that a grant made through a path which forgets is visible as a grant nothing can be restored to, rather than one an operator can quietly widen.
func (*Session) WithGroups ¶ added in v0.13.0
WithGroups records the group memberships that authorized this grant and returns the session, for chaining at the consent call sites.
type SessionData ¶
type SessionData struct {
Username string // Authenticated username
Groups []string // User groups from IDP (for scope filtering)
CreatedAt time.Time // When the session was created
ExpiresAt time.Time // When the session expires
}
SessionData represents the data stored in a session.
Note: this struct used to carry a Token field that was reserved for per-session HTCondor token storage. The column was never written to and the field is gone — the schema migration in 0002_envelope_encryption.sql drops `http_sessions.token` to remove the unused secret-shaped column. Per-user tokens, when needed, flow through the OAuth2 / IDP storage tables instead.
type SessionStore ¶
type SessionStore struct {
// contains filtered or unexported fields
}
SessionStore manages HTTP sessions with SQLite persistence
func NewSessionStore ¶
NewSessionStore creates a new session store with database persistence The db parameter should be the same database connection used by OAuth2Storage
func (*SessionStore) Create ¶
func (s *SessionStore) Create(username string, groups ...[]string) (string, *SessionData, error)
Create creates a new session for the given username and groups
func (*SessionStore) Delete ¶
func (s *SessionStore) Delete(sessionID string)
Delete removes a session
func (*SessionStore) Get ¶
func (s *SessionStore) Get(sessionID string) *SessionData
Get retrieves a session by ID Returns nil if session doesn't exist or has expired
func (*SessionStore) Size ¶
func (s *SessionStore) Size() int
Size returns the number of active sessions
type ShareInputResponse ¶ added in v0.17.0
type ShareInputResponse struct {
}
ShareInputResponse is what a mint call returns. Always a list, even for a single proc: HTCondor spools per proc, so "the upload URL for this submission" is inherently plural, and one shape is easier to consume than a response that changes with the request.
Owner and the expiry are shared by every URL in one response; only the URL and its allow-set vary per proc.
type ShareInputUpload ¶ added in v0.17.0
type ShareInputUpload struct {
}
ShareInputUpload is one job's upload URL.
ExpectedFiles is the load-bearing field: the schedd accepts only the names in a job's allow-set and drops the rest of the tar in silence, and whoever redeems this URL is often not the person who wrote the submit file. Without the list they have no way to learn which names are expected until the job fails at execute time on a missing file. It is per-proc because the allow-set is: procs of one cluster can list different inputs.
type ShareOutputRequest ¶
type ShareOutputRequest struct {
}
ShareOutputRequest is the body for POST /api/v1/jobs/{id}/output/share.
type ShareOutputResponse ¶
type ShareOutputResponse struct {
}
ShareOutputResponse is what the SPA gets back. Owner is echoed for UX so the share preview can label the URL with "downloads as <owner>".
type ShareWatchRequest ¶ added in v0.17.0
type ShareWatchRequest struct {
}
ShareWatchRequest is the body for POST /api/v1/watches/{id}/share.
type ShareWatchResponse ¶ added in v0.17.0
type ShareWatchResponse struct {
// MaxWaitSeconds is what the redeem endpoint will block for, so a
// poller can size its own client timeout from the answer rather than
// from a number hard-coded on its side.
}
ShareWatchResponse is what a mint call returns.
type SharedInputResult ¶ added in v0.17.0
type SharedInputResult struct {
}
SharedInputResult is what a redeemed upload returns. Unexpected names the schedd dropped are reported rather than swallowed -- see ShareInputResponse.ExpectedFiles.
type SuperuserModeRequest ¶ added in v0.13.0
type SuperuserModeRequest struct {
Enabled bool `json:"enabled"`
}
SuperuserModeRequest toggles superuser mode for the calling session.
type SuperuserModeResponse ¶ added in v0.13.0
type SuperuserModeResponse struct {
Active bool `json:"active"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
// Identity is what the server will authenticate to the schedd as while
// the mode is on, and ActorIsQueueSuperUser whether that is the caller
// themselves. Surfaced because the two differ in how well the action
// can be attributed: as themselves, the schedd records the human; via
// the shared account, only this server and the job's reason string do.
Identity string `json:"identity,omitempty"`
ActorIsQueueSuperUser bool `json:"actor_is_queue_superuser,omitempty"`
// Note explains a fallback when one happened, including how to fix it.
// Empty when the operator is acting as themselves.
Note string `json:"note,omitempty"`
}
SuperuserModeResponse reports the resulting state.
type TokenCache ¶
type TokenCache struct {
// contains filtered or unexported fields
}
TokenCache manages validated tokens and their associated session caches
func (*TokenCache) Add ¶
func (tc *TokenCache) Add(token string) (*TokenCacheEntry, error)
Add adds a validated token to the cache with a session cache If the token is already in the cache, returns the existing entry Automatically schedules cleanup when the token expires
func (*TokenCache) AddValidated ¶
func (tc *TokenCache) AddValidated(token, username string, expiration time.Time) (*TokenCacheEntry, error)
AddValidated adds a pre-validated token (e.g. opaque token) to the cache
func (*TokenCache) Get ¶
func (tc *TokenCache) Get(token string) (*TokenCacheEntry, bool)
Get retrieves a token cache entry if it exists and is not expired
func (*TokenCache) MarkValidated ¶
func (tc *TokenCache) MarkValidated(token, authoritativeUsername string)
MarkValidated promotes a cached token to "validated" status, meaning a schedd op has authenticated successfully with it. Callers may optionally pass an authoritativeUsername observed from the schedd handshake — if non-empty and different from the JWT-claimed username, the entry is updated to the schedd-authoritative value (this protects against any case where the unverified sub claim disagreed with the schedd's interpretation).
Idempotent: safe to call repeatedly per request.
func (*TokenCache) Remove ¶
func (tc *TokenCache) Remove(token string)
Remove removes a token from the cache and cancels its cleanup timer
func (*TokenCache) ValidatedUsername ¶
func (tc *TokenCache) ValidatedUsername(token string) string
ValidatedUsername returns the username for a token only if it has been marked validated via a successful schedd handshake. Use this in code paths that must rely on authoritative identity (job-owner filtering, share-URL minting, audit logs). For loose use cases (rate-limit bucket key) the Get-and-read-Username pattern is fine.
Returns "" if the token is unknown, expired, or not yet validated.
type TokenCacheEntry ¶
type TokenCacheEntry struct {
Token string
Username string // sub from the JWT — unverified until Validated == true
Validated bool // true once a schedd op authenticated successfully with this token
Expiration time.Time
SessionCache *security.SessionCache
// contains filtered or unexported fields
}
TokenCacheEntry represents a cached token with its expiration and associated session cache.
Identity-trust note: Username is parsed from the JWT WITHOUT verifying the signature (we have no local way to verify — the only authoritative validator is the schedd's CEDAR handshake, which happens later when we make a schedd call). Until that handshake succeeds, the Username reflects whatever the client put in the token's `sub` claim and MUST NOT be used as authoritative identity (e.g. for filtering jobs to "owned by me", recording the Owner when minting a share URL, or any other authorization decision).
Validated reports whether at least one schedd op has succeeded with this token. Code paths that need authoritative identity should gate on Validated; code paths that only need a stable bucket key (rate-limit per-token / per-username) can use Username directly.
type UpstreamRefreshMode ¶ added in v0.18.0
type UpstreamRefreshMode string
UpstreamRefreshMode decides whether that credential is kept and used.
const ( // UpstreamRefreshAuto keeps and uses the credential when the provider // hands one over, and does nothing when it does not. // // The default, because whether a provider releases offline_access is // the provider's decision and not every one will. What this server // ASKS for is already the operator's to set (HTTP_API_OAUTH2_SCOPES); // auto does not add to that request, so enabling this cannot break a // login against a provider that rejects a scope it does not know. UpstreamRefreshAuto UpstreamRefreshMode = "auto" // UpstreamRefreshOn is auto plus a complaint: a provider that returns // no refresh token is a misconfiguration the operator asked to hear // about, rather than a silent fallback to never checking. UpstreamRefreshOn UpstreamRefreshMode = "on" // UpstreamRefreshOff never stores the credential. For a deployment // that would rather not hold one at all, which is a defensible // position: it is a long-lived key to somebody else's identity // provider, and the account-database path covers the same ground // where an account database exists. UpstreamRefreshOff UpstreamRefreshMode = "off" )
func ParseUpstreamRefreshMode ¶ added in v0.18.0
func ParseUpstreamRefreshMode(raw string) (UpstreamRefreshMode, error)
ParseUpstreamRefreshMode reads HTTP_API_UPSTREAM_REFRESH.
type UserInfo ¶
type UserInfo struct {
Subject string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Groups interface{} `json:"groups"` // Can be []string or string
Claims map[string]interface{} // Additional claims
}
UserInfo represents user information from the IDP
type UserRecordLookup ¶ added in v0.13.0
type UserRecordLookup interface {
GetUserRecord(ctx context.Context, user string) (*htcondor.UserRecord, error)
}
UserRecordLookup is the slice of the schedd client that UserRecordOracle needs. It exists so the oracle's semantics — above all what a missing record means — can be tested without a live schedd, since that distinction is the difference between "new user" and "locked out".
type UserRecordOracle ¶ added in v0.13.0
type UserRecordOracle struct {
// Lookup supplies the current schedd client. It is a function rather
// than a value because the handler re-creates its Schedd when the
// daemon's address changes.
Lookup func() UserRecordLookup
// UIDDomain qualifies bare usernames before the lookup; schedd records
// are keyed on the fully-qualified "user@domain" form.
UIDDomain string
Logger *logging.Logger
// Strict makes "no record at all" a revocation instead of no opinion.
//
// Off by default, because the schedd creates records lazily on first
// submit: in an ordinary pool "no record" means "has never submitted
// here", and revoking on it would cut off every new user.
//
// It becomes correct — and much stronger — in a pool that provisions a
// record for every user up front, since `condor_qusers -add <user>`
// (ENABLE_USERREC with the create option) creates one without the user
// submitting anything. There, absence really does mean "not a user of
// this AP", and `condor_qusers -delete` becomes a revocation an admin
// can perform. Do not enable it otherwise.
Strict bool
}
UserRecordOracle reports a user revoked when the schedd's per-user record says so — that is, when an admin has run `condor_qusers -disable <user> -reason "..."`.
This is the stronger of the two schedd-backed oracles, because it reads an explicit administrative act rather than inferring one from an ACL. The record's DisableReason is carried through to the OAuth2 error, so the operator's note reaches whoever is looking at the failure.
Absence of a record is deliberately NOT a denial. The schedd creates a record the first time a user submits, so "no record" routinely means "this user has never submitted here" — denying on it would lock out every new user and everyone on a freshly built pool. Set Strict only where every user is provisioned up front with `condor_qusers -add`.
func (*UserRecordOracle) Check ¶ added in v0.13.0
func (o *UserRecordOracle) Check(ctx context.Context, username string, _ []string) (ReauthDecision, error)
Check implements RevocationOracle.
func (*UserRecordOracle) Name ¶ added in v0.13.0
func (o *UserRecordOracle) Name() string
Name implements RevocationOracle.
type UserStatus ¶ added in v0.13.0
type UserStatus int
UserStatus is an oracle's verdict on whether a user is still entitled to hold a grant issued to them earlier.
const ( // UserStatusUnknown means the oracle has no opinion — it could not // reach its backing store, or the backing store has no record of this // user. It is NOT a denial: absence of a record is routinely // indistinguishable from "this user has simply never been seen here", // and denying on it would lock out every user of a fresh pool. UserStatusUnknown UserStatus = iota // UserStatusActive means the oracle affirmatively vouches for the user. UserStatusActive // UserStatusRevoked means the oracle affirmatively says this user may // no longer hold a grant. Only this verdict revokes. UserStatusRevoked )
func (UserStatus) String ¶ added in v0.13.0
func (s UserStatus) String() string
type VersionResponse ¶
type VersionResponse struct {
Version string `json:"version"`
Commit string `json:"commit"`
// StartTime is when this server process came up, RFC3339 in UTC.
StartTime string `json:"start_time"`
// UptimeSeconds is StartTime expressed as an elapsed duration, so a
// caller does not have to trust its own clock to agree with ours.
UptimeSeconds int64 `json:"uptime_seconds"`
}
VersionResponse represents a build-info response.
type WhoAmIResponse ¶
type WhoAmIResponse struct {
Authenticated bool `json:"authenticated"`
User string `json:"user,omitempty"` // Omit if not authenticated
}
WhoAmIResponse represents a whoami response
Source Files
¶
- advertise.go
- apikey_auth.go
- apikey_condor.go
- apikey_scope_gate.go
- apikey_store.go
- auth.go
- clientip.go
- credd_discovery.go
- dag_graph.go
- dashboard_goodput.go
- dashboard_mirror.go
- dashboard_snapshot.go
- dbmirror_token.go
- device_code_handler.go
- diagnostics.go
- groupset.go
- groupsource.go
- handler.go
- handlers.go
- handlers_admin.go
- handlers_admin_api_keys.go
- handlers_chat.go
- handlers_chat_doc_tools.go
- handlers_chat_job_detail_tools.go
- handlers_chat_slots_tool.go
- handlers_chat_submit_tools.go
- handlers_chat_tools.go
- handlers_credd.go
- handlers_dashboard_stream.go
- handlers_dbmirror.go
- handlers_dbmirror_test_query.go
- handlers_dbroute.go
- handlers_history.go
- handlers_interactive.go
- handlers_issues.go
- handlers_jobwatch.go
- handlers_jupyter.go
- handlers_log.go
- handlers_match_analysis.go
- handlers_metrics.go
- handlers_peek.go
- handlers_placement.go
- handlers_share.go
- handlers_ssh.go
- handlers_templates.go
- handlers_watch.go
- handlers_watch_share.go
- handlers_webui.go
- holdreason.go
- identity_cookie.go
- identity_group_oracle.go
- identity_index_store.go
- identity_local.go
- idp_handlers.go
- idp_provider.go
- idp_storage.go
- issues_collect.go
- job_usage_attrs.go
- jobaction_status.go
- jobwatch_poll.go
- jobwatch_source.go
- jupyter_store.go
- login_rate_limiter.go
- masterkey.go
- mcp_actor.go
- mcp_deadline.go
- mcp_handlers.go
- mcp_listener.go
- mcp_sdk_route.go
- mcp_token_mint.go
- metrics.go
- oauth2_callback_path.go
- oauth2_cimd.go
- oauth2_client_grants.go
- oauth2_client_provenance.go
- oauth2_client_refresh.go
- oauth2_oracles.go
- oauth2_provider.go
- oauth2_reauth.go
- oauth2_retention.go
- oauth2_session.go
- oauth2_sso.go
- oauth2_state.go
- oauth2_storage.go
- oauth2_token_exchange.go
- oauth2_token_exchange_ext.go
- openapi.go
- ping_health.go
- placementd_discovery.go
- pool_summary.go
- reconfig.go
- remoteaccess.go
- required_creds.go
- routes.go
- seal_setup.go
- server.go
- session.go
- sharedcompute.go
- submit_string_validate.go
- superuser_context.go
- superuser_policy.go
- superuser_session.go
- tarnames.go
- tarvalidate.go
- test_helpers.go
- tls_reloader.go
- upstream_oracle.go
- upstream_refresh.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package apikey implements the wire format and crypto for HTTP API authentication tokens this server issues for non-interactive callers (Prometheus, scripts, CI).
|
Package apikey implements the wire format and crypto for HTTP API authentication tokens this server issues for non-interactive callers (Prometheus, scripts, CI). |
|
Package appdb owns the single SQLite database the HTTP API server uses for OAuth2/MCP storage, the embedded IDP, browser sessions, and user-saved batch-submission templates.
|
Package appdb owns the single SQLite database the HTTP API server uses for OAuth2/MCP storage, the embedded IDP, browser sessions, and user-saved batch-submission templates. |
|
seal
Package seal provides envelope encryption for sensitive columns in the unified application database.
|
Package seal provides envelope encryption for sensitive columns in the unified application database. |
|
Package chat provides the LLM-backed chat endpoint that powers the "Ask about your jobs" surface in the SPA.
|
Package chat provides the LLM-backed chat endpoint that powers the "Ask about your jobs" surface in the SPA. |
|
Package jupyterhelperbin without the embed_jupyter_helper tag is a stub that reports "not embedded" — this is the default for `go build ./...` and for dev workflows that don't want a long Makefile dance every time the api binary is rebuilt.
|
Package jupyterhelperbin without the embed_jupyter_helper tag is a stub that reports "not embedded" — this is the default for `go build ./...` and for dev workflows that don't want a long Makefile dance every time the api binary is rebuilt. |
|
Package webui provides the embedded Next.js static export.
|
Package webui provides the embedded Next.js static export. |