web_ui

package
v0.0.0-...-9bbc400 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 89 Imported by: 0

README

A short README explaining our authorization permissions, specifically regarding tokens received from the URL or Header vs the login cookie.

Tokens that are part of the HTTP Request Header e.g. {"Authorization": "Bearer +"<token>} and that are set in the URL Query via Authz are considered valid if they are signed by either the Federation jwk or the Origin jwk.

However, tokens that are retrieved from the login cookie ctx.Cookie("login") are ONLY valid if the are signed by the Origin jwk. This can be seen in the prometheus code and how it accesses the functions in Authorization.go

Documentation

Overview

web_ui/middleware.go

**************************************************************

*
* Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License.  You may
* obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**************************************************************

Index

Constants

View Source
const (
	GroupSourceTypeOIDC     string = "oidc"
	GroupSourceTypeFile     string = "file"
	GroupSourceTypeInternal string = "internal"
	GroupSourceTypeGitHub   string = "github"
)

Group source types

View Source
const (
	MaxLabelLimit            = 128
	MaxLabelNameLengthLimit  = 256
	MaxLabelValueLengthLimit = 4096
	MaxSampleLimit           = 800
)

Variables

View Source
var ErrNotReady = errors.New("Scrape manager not ready")

ErrNotReady is returned if the underlying scrape manager is not ready yet.

Functions

func AdminAuthHandler

func AdminAuthHandler(ctx *gin.Context)

AdminAuthHandler checks the admin status of a logged-in user. This middleware should be cascaded behind the web_ui.AuthHandler

func AuthHandler

func AuthHandler(ctx *gin.Context)

Check if user is authenticated by checking if the "login" cookie is present and set the user identity to ctx

func CaptureAuthMethod

func CaptureAuthMethod(ctx *gin.Context) (database.AuthMethod, string)

captureAuthMethod inspects the request and reports how the calling user was authenticated. This is intended for *audit fields* on records the handler is about to create (e.g. invite links, API token activity logs); it is not an authorization decision.

Returns ("", "") if no recognised credential is present — the caller should still be running behind AuthHandler, so this only happens when we're called outside an authenticated route or with a malformed request.

API token IDs are the 5-character prefix of the `<id>.<secret>` token format (see api_token.ApiTokenRegex). Web-cookie sessions don't have a stable session id today, so AuthMethodID stays empty for that case. CaptureAuthMethod is the exported counterpart to captureAuthMethod for callers in sibling packages (e.g. origin/collections.go) that need to record the same audit fields when minting their own records (ownership-invite links, etc.). It just delegates.

func CheckAdmin

func CheckAdmin(identity UserIdentity) (isAdmin bool, message string)

CheckAdmin reports whether the identity holds the server.admin scope. The decision is delegated to EffectiveScopesForIdentity, which unions DB-stored user_scopes/group_scopes grants with the historical config-derived sources (Server.UIAdminUsers, Server.AdminGroups, the built-in "admin" username).

All matches are username-based — never ID, never OIDC Sub — so a malicious IdP cannot mint a token with sub == an admin's name and inherit privileges. See the user/group design doc for the reason usernames are the only authorization handle.

func CheckCollectionAdmin

func CheckCollectionAdmin(identity UserIdentity) (bool, string)

CheckCollectionAdmin reports whether the identity holds the server.collection_admin scope. server.admin implies it.

func CheckUserAdmin

func CheckUserAdmin(identity UserIdentity) (bool, string)

CheckUserAdmin reports whether the identity holds the server.user_admin scope. server.admin implies it (the implication is applied inside EffectiveScopesForIdentity).

func ConfigOAuthClientAPIs

func ConfigOAuthClientAPIs(engine *gin.Engine) error

Configure OAuth2 client and register related authentication endpoints for Web UI

func ConfigureEmbeddedPrometheus

func ConfigureEmbeddedPrometheus(ctx context.Context, engine *gin.Engine, dialContextFunc func(context.Context, string, string) (net.Conn, error)) error

func ConfigureServerWebAPI added in v1.0.4

func ConfigureServerWebAPI(ctx context.Context, engine *gin.Engine, egrp *errgroup.Group) error

Configure endpoints for server web APIs. This function does not configure any UI specific paths but just redirect root path to /view.

You need to mount the static resources for UI in a separate function

func CurrentAUPVersion

func CurrentAUPVersion() (path string, version string, err error)

CurrentAUPVersion is a thin convenience wrapper around resolveAUP for callers that only need the version string (e.g. the AUP gate). Keeps the file path in the result for diagnostic logging — empty when Server.AUPFile is unset (default) or "none" (disabled).

func DowntimeAuthHandler

func DowntimeAuthHandler(ctx *gin.Context)

DowntimeAuthHandler allows EITHER: 1. Admin cookie authentication (req from this server itself), OR 2. Server bearer token authentication (req from another server, i.e. origin/cache)

func EffectiveScopesForIdentity

func EffectiveScopesForIdentity(identity UserIdentity) []token_scopes.TokenScope

EffectiveScopesForIdentity returns the union of every scope the user holds. Combines DB-stored grants with config-derived ones so the historical Server.UIAdminUsers / Server.AdminGroups / ... config keys keep working without each operator having to migrate them into user_scopes rows.

The set is deduplicated and filtered to user-grantable scopes (token_scopes.IsUserGrantable). Returns an empty slice for an empty/anonymous identity.

func GenerateCSRFCookie

func GenerateCSRFCookie(ctx *gin.Context, metadata map[string]string) (string, error)

Generate a 16B random string and set as the value of ctx session key "oauthstate" return a string for OAuth2 "state" query parameter including the random string and other metadata

func GenerateOAuthState

func GenerateOAuthState(metadata map[string]string) string

Generate the state for the authentication request in OAuth2 code flow. The metadata are formatted similar to url query parameters:

key1=val1&key2=val2

where values are url-encoded. We then base64 encode the resulting string in order to ensure that over-zealous providers do not treat the final URL as a double-encoding attack or somesuch.

func GetEngine

func GetEngine() (*gin.Engine, error)

func GetSessionHandler

func GetSessionHandler() (gin.HandlerFunc, error)

Setup and return the session handler for web UI APIs. Calling multiple times will only set up the handler once

func GetUserGroups

func GetUserGroups(ctx *gin.Context) (user string, userId string, groups []string, err error)

Get user information including userId from the login cookie or Bearer token. Returns username, userId, sub, issuer, groups, and error.

func HandleCreateDowntime

func HandleCreateDowntime(ctx *gin.Context)

func HandleDeleteDowntime

func HandleDeleteDowntime(ctx *gin.Context)

func HandleDeleteLogLevel

func HandleDeleteLogLevel(ctx *gin.Context)

HandleDeleteLogLevel handles DELETE requests to remove a temporary log level change

func HandleGetDowntime

func HandleGetDowntime(ctx *gin.Context)

func HandleGetDowntimeByUUID

func HandleGetDowntimeByUUID(ctx *gin.Context)

func HandleGetFederationInfo

func HandleGetFederationInfo(ctx *gin.Context)

HandleGetFederationInfo returns the resolved federation discovery info (director, registry, JWK URLs, etc.) for any authenticated user. These values are otherwise only reachable via the admin-only /config endpoint, which prevents non-admin users from rendering basic federation links on the dashboard. The values are not sensitive: directors publish them at /.well-known/pelican-configuration for anonymous client discovery.

func HandleGetLogLevel

func HandleGetLogLevel(ctx *gin.Context)

HandleGetLogLevel handles GET requests to retrieve current log level status

func HandleGetServerLocalMetadata

func HandleGetServerLocalMetadata(ctx *gin.Context)

HandleGetServerLocalMetadata returns the most recent locally cached server metadata entry (just the current name/type) for Origins/Caches.

func HandleGetServerLocalMetadataHistory

func HandleGetServerLocalMetadataHistory(ctx *gin.Context)

HandleGetServerLocalMetadataHistory returns the locally cached server metadata history for Origins/Caches.

func HandleSetLogLevel

func HandleSetLogLevel(ctx *gin.Context)

HandleSetLogLevel handles POST requests to temporarily change log level

func HandleUpdateDowntime

func HandleUpdateDowntime(ctx *gin.Context)

func InitServerWebLogin added in v1.0.4

func InitServerWebLogin(ctx context.Context) error

Setup the initial server web login by sending the one-time code to stdout and record health status of the WebUI based on the success of the initialization

func IsSystemAdminUserID

func IsSystemAdminUserID(db *gorm.DB, userID string) bool

IsSystemAdminUserID checks whether the given user ID belongs to a system admin. This is used to prevent user administrators from modifying system admin accounts.

func ParseOAuthState

func ParseOAuthState(state string) (metadata map[string]string, err error)

Parse the OAuth2 callback state into a key-val map. Error if keys are duplicated state is the url-decoded value of the query parameter "state" in the the OAuth2 callback request

func ReadOnlyMiddleware

func ReadOnlyMiddleware(ctx *gin.Context)

func RegisterAuthEndpoints

func RegisterAuthEndpoints(ctx context.Context, routerGroup *gin.RouterGroup, egrp *errgroup.Group) error

Configure the authentication endpoints for the server web UI

func RequireAUPCompliance

func RequireAUPCompliance(ctx *gin.Context)

RequireAUPCompliance is a gate that runs after AuthHandler and blocks the request when the caller has not yet agreed to the current AUP.

AUP signing is a hard precondition for using the system per the Pelican design contract: a user who refuses must not be able to proceed with anything other than (a) reading the AUP, (b) signing it, or (c) logging out. Those endpoints are intentionally NOT wrapped in this middleware; everything else that touches user-affecting state is.

The 403 body carries `requires_aup: true` so the frontend can route the user to the AUP page; the version field matches the one returned by /whoami, so the UI can compute "this is the version I need to sign" without a separate fetch.

func RequireAuthMiddleware

func RequireAuthMiddleware(ctx *gin.Context)

Require auth; if missing, redirect to the login endpoint.

The current implementation forces the OAuth2 endpoint; future work may instead use a generic login page.

func RunEngine

func RunEngine(ctx context.Context, engine *gin.Engine, egrp *errgroup.Group) error

Run the gin engine in the current goroutine.

Will use a background golang routine to periodically reload the certificate utilized by the UI.

func RunEngineRoutine

func RunEngineRoutine(ctx context.Context, engine *gin.Engine, egrp *errgroup.Group, curRoutine bool) error

Run the gin engine; if curRoutine is false, it will run in a background goroutine.

func RunEngineRoutineWithListener

func RunEngineRoutineWithListener(ctx context.Context, engine *gin.Engine, egrp *errgroup.Group, curRoutine bool, ln net.Listener) error

Run the web engine connected to a provided listener `ln`.

func ServerHeaderMiddleware

func ServerHeaderMiddleware(ctx *gin.Context)

func UserAdminAuthHandler

func UserAdminAuthHandler(ctx *gin.Context)

UserAdminAuthHandler accepts callers whose effective scope set contains EITHER server.admin OR server.user_admin. It is the route-level gate for the /api/v1.0/users/* surface and the onboarding-invite endpoint: surfaces a system administrator must be able to use, and which a "user administrator" (per the design contract: manage non-admin users and unprivileged groups) is also expected to use. Per-target guards inside the handlers (notably IsSystemAdminUserID) prevent a user-admin from acting on a system-admin account; this gate just decides who clears the door.

Cascade behind AuthHandler (cookie/bearer parsing must have run first so the identity is in context).

func WritePasswordEntry added in v1.0.4

func WritePasswordEntry(user, password string) error

Types

type AUPDocumentResp

type AUPDocumentResp struct {
	Content      string    `json:"content"`
	Version      string    `json:"version"`
	Source       AUPSource `json:"source"`
	LastUpdated  string    `json:"lastUpdated,omitempty"`
	CanonicalURL string    `json:"canonicalUrl,omitempty"`
	// Editable indicates that the active copy is operator-managed
	// in-DB (Source == AUPSourceDB) — the Settings UI uses this to
	// decide whether to show "you are editing the active version" vs
	// "you are about to override the embedded/file default with a
	// fresh DB copy."
	Editable bool `json:"editable"`
}

AUPDocumentResp is everything a caller needs to render the AUP: the content (Markdown), the version hash, the source the content came from, and the optional last-updated / canonical-link footer fields.

type AUPSource

type AUPSource string

AUPSource describes where the served AUP came from. Useful so the UI can label which kind of policy is active and so logs make it obvious which copy was used during a given request.

const (
	AUPSourceNone     AUPSource = "none"     // explicitly disabled
	AUPSourceDefault  AUPSource = "default"  // embedded default
	AUPSourceOperator AUPSource = "operator" // file pointed at by Server.AUPFile
	AUPSourceDB       AUPSource = "db"       // operator-edited copy in aup_documents
)

type AddGroupMemberReq

type AddGroupMemberReq struct {
	UserID string `json:"userId"`
}

type AddUserIdentityReq

type AddUserIdentityReq struct {
	Sub    string `json:"sub"`
	Issuer string `json:"issuer"`
}

type AddUserReq

type AddUserReq struct {
	Username    string `json:"username"`
	Sub         string `json:"sub,omitempty"`
	Issuer      string `json:"issuer,omitempty"`
	DisplayName string `json:"displayName,omitempty"`
}

AddUserReq describes a user-create request. There are two flavors:

  • Local user (username/password login): supply Username (and optionally DisplayName). The user is created without a password; the admin then mints a password-set invite (POST /users/:id/password-invite) and hands the link to the user, who picks their own password. Admins never see or set passwords directly — that's the whole point.
  • External (OIDC) user: supply Username, Sub, and Issuer.

type CreateApiTokenReq

type CreateApiTokenReq struct {
	Name       string   `json:"name"`
	Expiration string   `json:"expiration"` // RFC3339 format, if not provided or "never" or "", token will not expire
	Scopes     []string `json:"scopes"`
}

type CreateGroupReq

type CreateGroupReq struct {
	// Name is the machine-readable group identifier. Goes into policy
	// strings, admin-group lists, and ACL grants. Validated by
	// ValidateIdentifier.
	Name string `json:"name"`
	// DisplayName is the human-friendly label rendered in the UI.
	// Optional; UIs typically fall back to Name when empty.
	DisplayName string `json:"displayName,omitempty"`
	Description string `json:"description"`
	// CreatedForCollectionID ties the new group to a specific collection's
	// onboarding pass. Set by the collection-onboarding form so a later
	// ownership transfer of that collection cascades to its onboarded
	// groups; empty for standalone group creation.
	CreatedForCollectionID string `json:"createdForCollectionId,omitempty"`
	// AuthTemplateEligible asks the server to mark this new group as
	// eligible to match Issuer.AuthorizationTemplates and the
	// Server.*AdminGroups config lists. Only an admin / user-admin
	// caller may set this to true — for everyone else the request
	// field is silently forced to false (the handler enforces this).
	AuthTemplateEligible bool `json:"authTemplateEligible,omitempty"`
}

type CreateInviteLinkReq

type CreateInviteLinkReq struct {
	IsSingleUse bool   `json:"isSingleUse"`
	ExpiresIn   string `json:"expiresIn"` // Duration string, e.g. "168h", "7d"
}

type DowntimeInput

type DowntimeInput struct {
	CreatedBy   string                  `json:"createdBy"`  // Person who created this downtime
	UpdatedBy   string                  `json:"updatedBy"`  // Person who last updated this downtime
	ServerName  string                  `json:"serverName"` // Empty for Origin/Cache input; Not empty for Registry input
	ServerID    string                  `json:"serverId"`
	Source      string                  `json:"source"` // Automatically set by the server; should only be set by input during testing
	Class       server_structs.Class    `json:"class"`
	Description string                  `json:"description"`
	Severity    server_structs.Severity `json:"severity"`
	StartTime   int64                   `json:"startTime"` // Epoch UTC in seconds
	EndTime     int64                   `json:"endTime"`   // Epoch UTC in seconds
}

type GroupView

type GroupView struct {
	database.Group
	OwnerUser     *database.UserCard  `json:"ownerUser,omitempty"`
	AdminUser     *database.UserCard  `json:"adminUser,omitempty"`
	AdminGroup    *database.GroupCard `json:"adminGroup,omitempty"`
	CreatedByUser *database.UserCard  `json:"createdByUser,omitempty"`
}

GroupView wraps a database.Group with resolved user/group summaries for owner, admin (when adminType=user), createdBy, and (when adminType=group) the admin group itself. The frontend renders these as "Display Name (username)" without needing follow-up lookups, and without requiring user-listing privileges.

type InitLogin added in v1.0.4

type InitLogin struct {
	Code string `form:"code"`
}

type InviteLinkView

type InviteLinkView struct {
	database.GroupInviteLink
	RedeemedByUser *database.UserCard `json:"redeemedByUser,omitempty"`
}

InviteLinkView wraps a GroupInviteLink with a resolved RedeemedByUser summary. Embedding the card avoids a follow-up GET /users/:id call (which most callers don't have permission for) and lets the UI render the same UserPill component used elsewhere.

type LogLevelChangeResponse

type LogLevelChangeResponse struct {
	ChangeID        string     `json:"changeId"`
	Level           string     `json:"level"`
	ParameterName   string     `json:"parameterName,omitempty"`
	EndTime         time.Time  `json:"endTime"`
	Remaining       int        `json:"remainingSeconds"`
	RequiresRestart bool       `json:"requiresRestart,omitempty"`
	EffectiveAt     *time.Time `json:"effectiveAt,omitempty"`
}

LogLevelChangeResponse represents a log level change with its metadata

type LogLevelStatusResponse

type LogLevelStatusResponse struct {
	CurrentLevel  string                   `json:"currentLevel"`
	BaseLevel     string                   `json:"baseLevel"`
	ActiveChanges []LogLevelChangeResponse `json:"activeChanges"`
	Parameters    []ParameterLevelStatus   `json:"parameters"`
}

LogLevelStatusResponse represents the current log level status

type Login added in v1.0.4

type Login struct {
	User     string `form:"user"`
	Password string `form:"password"`
}

type LogrusAdapter added in v1.0.4

type LogrusAdapter struct {
	*logrus.Logger
	// contains filtered or unexported fields
}

func (LogrusAdapter) Log added in v1.0.4

func (a LogrusAdapter) Log(keyvals ...interface{}) error

Log method which satisfies the kitlog.Logger interface. It also propragates field level and field message to top level log

type OIDCEnabledServerRes

type OIDCEnabledServerRes struct {
	ODICEnabledServers []string `json:"oidc_enabled_servers"`
}

type ParameterLevelStatus

type ParameterLevelStatus struct {
	ParameterName string `json:"parameterName"`
	CurrentLevel  string `json:"currentLevel"`
	BaseLevel     string `json:"baseLevel"`
}

ParameterLevelStatus summarizes the current/base level for a parameter.

type PasswordReset added in v1.0.4

type PasswordReset struct {
	Password string `form:"password"`
}

type ReadyHandler

type ReadyHandler struct {
	// contains filtered or unexported fields
}

func (*ReadyHandler) SetReady

func (h *ReadyHandler) SetReady(v bool)

type RecordAUPAgreementReq

type RecordAUPAgreementReq struct {
	Version string `json:"version"`
}

type RedeemInviteLinkReq

type RedeemInviteLinkReq struct {
	Token string `json:"token"`
}

type SetLogLevelRequest

type SetLogLevelRequest struct {
	Level         string `json:"level" binding:"required"`          // Log level (e.g., "debug", "info", "warn", "error")
	Duration      int    `json:"duration" binding:"required,min=1"` // Duration in seconds
	ParameterName string `json:"parameterName"`                     // Parameter name like "Logging.Level" or "Logging.Origin.Xrootd"
}

SetLogLevelRequest represents a request to temporarily change log level

type UpdateGroupOwnershipReq

type UpdateGroupOwnershipReq struct {
	OwnerID   *string             `json:"ownerId"`
	AdminID   *string             `json:"adminId"`
	AdminType *database.AdminType `json:"adminType"`
}

type UpdateGroupReq

type UpdateGroupReq struct {
	Name        *string `json:"name"`
	DisplayName *string `json:"displayName"`
	Description *string `json:"description"`
	// AuthTemplateEligible may only be flipped by an admin or
	// user-admin (the database layer also enforces this). nil = no
	// change; &true / &false = explicit set.
	AuthTemplateEligible *bool `json:"authTemplateEligible"`
}

UpdateGroupReq carries optional updates for PATCH /groups/:id.

  • Name (machine identifier) may only be changed by a system admin. Owners/group-admins attempting it get 403.
  • DisplayName and Description are owner-editable.

type UpdateMeReq

type UpdateMeReq struct {
	DisplayName *string `json:"displayName"`
}

UpdateMeReq is the body for PATCH /me. Only displayName is mutable self-service; username/sub/issuer changes go through admin endpoints.

type UpdateMyPasswordReq

type UpdateMyPasswordReq struct {
	CurrentPassword string `json:"currentPassword"`
	NewPassword     string `json:"newPassword"`
}

UpdateMyPasswordReq is the body for PUT /me/password — a password rotation. Both fields are required: the caller proves they hold the current credential before being allowed to set a new one (a stolen cookie alone shouldn't be enough to lock the rightful owner out).

type UpdateUserReq

type UpdateUserReq struct {
	// Per the user-record contract (see comment on database.User):
	//   - Username is the authorization handle; admin-only renames go here.
	//   - DisplayName is a human label; admins can override one as a courtesy
	//     even though users normally edit their own via PATCH /me.
	//   - Sub/Issuer are linked OIDC identities; managed via
	//     /users/:id/identities, never directly here.
	Username    *string `json:"username"`
	DisplayName *string `json:"displayName"`
}

type UpdateUserStatusReq

type UpdateUserStatusReq struct {
	Status *database.UserStatus `json:"status"`
}

UpdateUserStatusReq is for the admin-only PUT /users/:id/status endpoint. Status flips an account between active and inactive — purely an authorization concern. Display name (a human label) used to ride along here; it now goes through PATCH /users/:id (admin) or PATCH /me (self), matching the user-record contract on database.User.

type UserIdentity

type UserIdentity struct {
	Username string
	ID       string
	Sub      string // OIDC Subject
	Groups   []string
}

UserIdentity encapsulates all available information about a user's identity

type UserRole

type UserRole string
const (
	AdminRole    UserRole = "admin"
	NonAdminRole UserRole = "user"
)

type WhoAmIRes

type WhoAmIRes struct {
	Authenticated bool     `json:"authenticated"`
	Role          UserRole `json:"role"`
	User          string   `json:"user"`
	// DisplayName is the human label from the User row (if any).
	// Surfaced here so the navbar's user menu can render it
	// without a second /me round-trip on every page mount.
	// Empty when the row has no display name set.
	DisplayName string `json:"displayName,omitempty"`
	// Scopes is the caller's effective user-grantable scope set
	// (DB user_scopes ∪ DB group_scopes via membership ∪
	// config-derived grants ∪ admin implications). Used by
	// the frontend to gate UI surfaces below the granularity of
	// Role: e.g. /settings/users/ is reachable by anyone with
	// server.user_admin (which is a subset of server.admin),
	// and the navbar toggles its visibility off scopes rather
	// than role membership. Empty for unauthenticated callers.
	Scopes      []string `json:"scopes,omitempty"`
	RequiresAUP bool     `json:"requires_aup,omitempty"`
	AUPVersion  string   `json:"aup_version,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL