mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: show service banner in SSH/TTY sessions (#8186)
* Allow workspace agents to get appearance * Poll for service banner every two minutes * Show service banner before MOTD if not quiet
This commit is contained in:
@ -704,7 +704,10 @@ func New(options *Options) *API {
|
||||
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
|
||||
r.Get("/connection", api.workspaceAgentConnectionGeneric)
|
||||
r.Route("/me", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
|
||||
r.Use(httpmw.ExtractWorkspaceAgent(httpmw.ExtractWorkspaceAgentConfig{
|
||||
DB: options.Database,
|
||||
Optional: false,
|
||||
}))
|
||||
r.Get("/manifest", api.workspaceAgentManifest)
|
||||
// This route is deprecated and will be removed in a future release.
|
||||
// New agents will use /me/manifest instead.
|
||||
|
@ -35,3 +35,32 @@ func RequireAPIKeyOrWorkspaceProxyAuth() func(http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAPIKeyOrWorkspaceAgent is middleware that should be inserted after
|
||||
// optional ExtractAPIKey and ExtractWorkspaceAgent middlewares to ensure one of
|
||||
// the two is provided.
|
||||
//
|
||||
// If both are provided an error is returned to avoid misuse.
|
||||
func RequireAPIKeyOrWorkspaceAgent() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, hasAPIKey := APIKeyOptional(r)
|
||||
_, hasWorkspaceAgent := WorkspaceAgentOptional(r)
|
||||
|
||||
if hasAPIKey && hasWorkspaceAgent {
|
||||
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "API key and workspace agent token provided, but only one is allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !hasAPIKey && !hasWorkspaceAgent {
|
||||
httpapi.Write(r.Context(), w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "API key or workspace agent token required, but none provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -18,40 +18,67 @@ import (
|
||||
|
||||
type workspaceAgentContextKey struct{}
|
||||
|
||||
func WorkspaceAgentOptional(r *http.Request) (database.WorkspaceAgent, bool) {
|
||||
user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
// WorkspaceAgent returns the workspace agent from the ExtractAgent handler.
|
||||
func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
|
||||
user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent)
|
||||
user, ok := WorkspaceAgentOptional(r)
|
||||
if !ok {
|
||||
panic("developer error: agent middleware not provided")
|
||||
panic("developer error: agent middleware not provided or was made optional")
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
type ExtractWorkspaceAgentConfig struct {
|
||||
DB database.Store
|
||||
// Optional indicates whether the middleware should be optional. If true, any
|
||||
// requests without the a token or with an invalid token will be allowed to
|
||||
// continue and no workspace agent will be set on the request context.
|
||||
Optional bool
|
||||
}
|
||||
|
||||
// ExtractWorkspaceAgent requires authentication using a valid agent token.
|
||||
func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
|
||||
func ExtractWorkspaceAgent(opts ExtractWorkspaceAgentConfig) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// optionalWrite wraps httpapi.Write but runs the next handler if the
|
||||
// token is optional.
|
||||
//
|
||||
// It should be used when the token is not provided or is invalid, but not
|
||||
// when there are other errors.
|
||||
optionalWrite := func(code int, response codersdk.Response) {
|
||||
if opts.Optional {
|
||||
next.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, code, response)
|
||||
}
|
||||
|
||||
tokenValue := APITokenFromRequest(r)
|
||||
if tokenValue == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||
optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
token, err := uuid.Parse(tokenValue)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||
optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Workspace agent token invalid.",
|
||||
Detail: fmt.Sprintf("An agent token must be a valid UUIDv4. (len %d)", len(tokenValue)),
|
||||
})
|
||||
return
|
||||
}
|
||||
//nolint:gocritic // System needs to be able to get workspace agents.
|
||||
agent, err := db.GetWorkspaceAgentByAuthToken(dbauthz.AsSystemRestricted(ctx), token)
|
||||
agent, err := opts.DB.GetWorkspaceAgentByAuthToken(dbauthz.AsSystemRestricted(ctx), token)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||
optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Workspace agent not authorized.",
|
||||
Detail: "The agent cannot authenticate until the workspace provision job has been completed. If the job is no longer running, this agent is invalid.",
|
||||
})
|
||||
@ -66,7 +93,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
//nolint:gocritic // System needs to be able to get workspace agents.
|
||||
subject, err := getAgentSubject(dbauthz.AsSystemRestricted(ctx), db, agent)
|
||||
subject, err := getAgentSubject(dbauthz.AsSystemRestricted(ctx), opts.DB, agent)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace agent.",
|
||||
|
@ -30,7 +30,10 @@ func TestWorkspaceAgent(t *testing.T) {
|
||||
db := dbfake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceAgent(db),
|
||||
httpmw.ExtractWorkspaceAgent(httpmw.ExtractWorkspaceAgentConfig{
|
||||
DB: db,
|
||||
Optional: false,
|
||||
}),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
r := setup(db, uuid.New())
|
||||
@ -65,7 +68,10 @@ func TestWorkspaceAgent(t *testing.T) {
|
||||
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceAgent(db),
|
||||
httpmw.ExtractWorkspaceAgent(httpmw.ExtractWorkspaceAgentConfig{
|
||||
DB: db,
|
||||
Optional: false,
|
||||
}),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceAgent(r)
|
||||
|
@ -257,3 +257,7 @@ func (*client) PostStartup(_ context.Context, _ agentsdk.PostStartupRequest) err
|
||||
func (*client) PatchStartupLogs(_ context.Context, _ agentsdk.PatchStartupLogs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*client) GetServiceBanner(_ context.Context) (codersdk.ServiceBannerConfig, error) {
|
||||
return codersdk.ServiceBannerConfig{}, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user