Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a dedicated API endpoint for app FQDN resolving #6449

Merged
merged 7 commits into from
Apr 26, 2021
Merged
4 changes: 2 additions & 2 deletions integration/app_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,8 +764,8 @@ func (p *pack) createAppSession(t *testing.T, publicAddr, clusterName string) st
require.NotEmpty(t, p.webCookie)
require.NotEmpty(t, p.webToken)

casReq, err := json.Marshal(web.CreateAppSessionRequest{
FQDN: publicAddr,
casReq, err := json.Marshal(web.AppResolveParams{
FQDNHint: publicAddr,
PublicAddr: publicAddr,
ClusterName: clusterName,
})
Expand Down
5 changes: 4 additions & 1 deletion lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
h.POST("/webapi/trustedcluster", h.WithAuth(h.upsertTrustedClusterHandle))
h.DELETE("/webapi/trustedcluster/:name", h.WithAuth(h.deleteTrustedCluster))

h.GET("/webapi/apps/:fqdnHint", h.WithAuth(h.getAppFQDN))
h.GET("/webapi/apps/:fqdnHint/:clusterName/:publicAddr", h.WithAuth(h.getAppFQDN))
andrejtokarcik marked this conversation as resolved.
Show resolved Hide resolved

// if Web UI is enabled, check the assets dir:
var indexPage *template.Template
if cfg.StaticFS != nil {
Expand Down Expand Up @@ -1246,7 +1249,7 @@ func newSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {

// createWebSession creates a new web session based on user, pass and 2nd factor token
//
// POST /v1/webapi/sessions
// POST /v1/webapi/sessions/web
//
// {"user": "alex", "pass": "abc123", "second_factor_token": "token", "second_factor_type": "totp"}
//
Expand Down
36 changes: 18 additions & 18 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1836,8 +1836,8 @@ func TestApplicationAccessDisabled(t *testing.T) {
require.NoError(t, err)

endpoint := pack.clt.Endpoint("webapi", "sessions", "app")
_, err = pack.clt.PostJSON(context.Background(), endpoint, &CreateAppSessionRequest{
FQDN: "panel.example.com",
_, err = pack.clt.PostJSON(context.Background(), endpoint, &AppResolveParams{
FQDNHint: "panel.example.com",
PublicAddr: "panel.example.com",
ClusterName: "localhost",
})
Expand Down Expand Up @@ -1882,15 +1882,15 @@ func (s *WebSuite) TestCreateAppSession(c *C) {

var tests = []struct {
inComment CommentInterface
inCreateRequest *CreateAppSessionRequest
inCreateRequest *AppResolveParams
outError bool
outFQDN string
outUsername string
}{
{
inComment: Commentf("Valid request: all fields."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "panel.example.com",
inCreateRequest: &AppResolveParams{
FQDNHint: "panel.example.com",
PublicAddr: "panel.example.com",
ClusterName: "localhost",
},
Expand All @@ -1900,7 +1900,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) {
},
{
inComment: Commentf("Valid request: without FQDN."),
inCreateRequest: &CreateAppSessionRequest{
inCreateRequest: &AppResolveParams{
PublicAddr: "panel.example.com",
ClusterName: "localhost",
},
Expand All @@ -1910,49 +1910,49 @@ func (s *WebSuite) TestCreateAppSession(c *C) {
},
{
inComment: Commentf("Valid request: only FQDN."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "panel.example.com",
inCreateRequest: &AppResolveParams{
FQDNHint: "panel.example.com",
},
outError: false,
outFQDN: "panel.example.com",
outUsername: "[email protected]",
},
{
inComment: Commentf("Invalid request: only public address."),
inCreateRequest: &CreateAppSessionRequest{
inCreateRequest: &AppResolveParams{
PublicAddr: "panel.example.com",
},
outError: true,
},
{
inComment: Commentf("Invalid request: only cluster name."),
inCreateRequest: &CreateAppSessionRequest{
inCreateRequest: &AppResolveParams{
ClusterName: "localhost",
},
outError: true,
},
{
inComment: Commentf("Invalid application."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "panel.example.com",
inCreateRequest: &AppResolveParams{
FQDNHint: "panel.example.com",
PublicAddr: "invalid.example.com",
ClusterName: "localhost",
},
outError: true,
},
{
inComment: Commentf("Invalid cluster name."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "panel.example.com",
inCreateRequest: &AppResolveParams{
FQDNHint: "panel.example.com",
PublicAddr: "panel.example.com",
ClusterName: "example.com",
},
outError: true,
},
{
inComment: Commentf("Malicious request: all fields."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "[email protected]",
inCreateRequest: &AppResolveParams{
FQDNHint: "[email protected]",
PublicAddr: "panel.example.com",
ClusterName: "localhost",
},
Expand All @@ -1962,8 +1962,8 @@ func (s *WebSuite) TestCreateAppSession(c *C) {
},
{
inComment: Commentf("Malicious request: only FQDN."),
inCreateRequest: &CreateAppSessionRequest{
FQDN: "[email protected]",
inCreateRequest: &AppResolveParams{
FQDNHint: "[email protected]",
},
outError: true,
},
Expand Down
11 changes: 6 additions & 5 deletions lib/web/app/fragment.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,22 @@ type fragmentRequest struct {
func (h *Handler) handleFragment(w http.ResponseWriter, r *http.Request, p httprouter.Params) error {
switch r.Method {
case http.MethodGet:
q := r.URL.Query()
// If the state query parameter is not set, generate a new state token,
// store it in a cookie and redirect back to the app launcher.
if r.URL.Query().Get("state") == "" {
if q.Get("state") == "" {
stateToken, err := utils.CryptoRandomHex(auth.TokenLenBytes)
if err != nil {
h.log.WithError(err).Debugf("Failed to generate and encode random numbers.")
return trace.AccessDenied("access denied")
}
h.setAuthStateCookie(w, stateToken)
p := launcherURLParams{
clusterName: r.URL.Query().Get("cluster"),
publicAddr: r.URL.Query().Get("addr"),
urlParams := launcherURLParams{
clusterName: q.Get("cluster"),
publicAddr: q.Get("addr"),
stateToken: stateToken,
}
return h.redirectToLauncher(w, r, p)
return h.redirectToLauncher(w, r, urlParams)
}

nonce, err := utils.CryptoRandomHex(auth.TokenLenBytes)
Expand Down
63 changes: 53 additions & 10 deletions lib/web/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ func (h *Handler) siteAppsGet(w http.ResponseWriter, r *http.Request, p httprout
return makeResponse(ui.MakeApps(h.auth.clusterName, h.proxyDNSName(), appClusterName, appServers))
}

type CreateAppSessionRequest struct {
// FQDN is the fully qualified domain name of the application.
FQDN string `json:"fqdn"`
type AppResolveParams struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this to AppResolveRequest for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this since these AppResolveParams are populated in the course of both getAppFQDN and createAppSession. Renaming this to AppResolveRequest would misleadingly indicate this is just the input part of a single request-response pair.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, please make wrapper types for individual endpoints, like:

type GetAppFQDNRequest AppResolveRequest

(or ember a struct, whichever is cleaner)

// FQDNHint indicates (tentatively) the fully qualified domain name of the application.
FQDNHint string `json:"fqdn"`

// PublicAddr is the public address of the application.
PublicAddr string `json:"public_addr"`
Expand All @@ -66,15 +66,58 @@ type CreateAppSessionRequest struct {
ClusterName string `json:"cluster_name"`
}

type GetAppFQDNResponse struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppResolveResponse for consistency?

// FQDN is application FQDN.
FQDN string `json:"fqdn"`
}

type CreateAppSessionResponse struct {
// CookieValue is the application session cookie value.
CookieValue string `json:"value"`
// FQDN is application FQDN.
FQDN string `json:"fqdn"`
}

// getAppFQDN resolves the input params to a known application and returns
// its valid FQDN.
//
// GET /v1/webapi/apps/:fqdn/:clusterName/:publicAddr
func (h *Handler) getAppFQDN(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext) (interface{}, error) {
req := &AppResolveParams{
FQDNHint: p.ByName("fqdnHint"),
andrejtokarcik marked this conversation as resolved.
Show resolved Hide resolved
ClusterName: p.ByName("clusterName"),
PublicAddr: p.ByName("publicAddr"),
}

// Get an auth client connected with the user's identity.
authClient, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

// Get a reverse tunnel proxy aware of the user's permissions.
proxy, err := h.ProxyWithRoles(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// Use the information the caller provided to attempt to resolve to an
// application running within either the root or leaf cluster.
result, err := h.resolveApp(r.Context(), authClient, proxy, req)
if err != nil {
return nil, trace.Wrap(err, "Unable to resolve FQDN: %v", req.FQDNHint)
andrejtokarcik marked this conversation as resolved.
Show resolved Hide resolved
}

return &GetAppFQDNResponse{
FQDN: result.FQDN,
}, nil
}

// createAppSession creates a new application session.
//
// POST /v1/webapi/sessions/app
func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext) (interface{}, error) {
var req *CreateAppSessionRequest
var req *AppResolveParams
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -93,9 +136,9 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt

// Use the information the caller provided to attempt to resolve to an
// application running within either the root or leaf cluster.
result, err := h.validateAppSessionRequest(r.Context(), authClient, proxy, req)
result, err := h.resolveApp(r.Context(), authClient, proxy, req)
if err != nil {
return nil, trace.Wrap(err, "Unable to resolve FQDN: %v", req.FQDN)
return nil, trace.Wrap(err, "Unable to resolve FQDN: %v", req.FQDNHint)
andrejtokarcik marked this conversation as resolved.
Show resolved Hide resolved
}

h.log.Debugf("Creating application web session for %v in %v.", result.PublicAddr, result.ClusterName)
Expand Down Expand Up @@ -175,7 +218,7 @@ func (h *Handler) waitForAppSession(ctx context.Context, sessionID, user string)
return auth.WaitForAppSession(ctx, sessionID, user, h.cfg.AccessPoint)
}

func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter, proxy reversetunnel.Tunnel, req *CreateAppSessionRequest) (*validateAppSessionResult, error) {
func (h *Handler) resolveApp(ctx context.Context, clt app.Getter, proxy reversetunnel.Tunnel, req *AppResolveParams) (*resolveAppResult, error) {
var (
app *services.App
server services.Server
Expand All @@ -189,23 +232,23 @@ func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter,
if req.PublicAddr != "" && req.ClusterName != "" {
app, server, appClusterName, err = h.resolveDirect(ctx, proxy, req.PublicAddr, req.ClusterName)
} else {
app, server, appClusterName, err = h.resolveFQDN(ctx, clt, proxy, req.FQDN)
app, server, appClusterName, err = h.resolveFQDN(ctx, clt, proxy, req.FQDNHint)
}
if err != nil {
return nil, trace.Wrap(err)
}

fqdn := ui.AssembleAppFQDN(h.auth.clusterName, h.proxyDNSName(), appClusterName, app)

return &validateAppSessionResult{
return &resolveAppResult{
ServerID: server.GetName(),
FQDN: fqdn,
PublicAddr: app.PublicAddr,
ClusterName: appClusterName,
}, nil
}

type validateAppSessionResult struct {
type resolveAppResult struct {
// ServerID is the ID of the server this application is running on.
ServerID string
// FQDN is the best effort FQDN resolved for this application.
Expand Down