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:
Asher
2023-06-30 10:41:29 -08:00
committed by GitHub
parent eb0497ff82
commit 6015319e9d
11 changed files with 706 additions and 216 deletions

View File

@ -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.

View File

@ -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)
})
}
}

View File

@ -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.",

View File

@ -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)

View File

@ -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
}