-
Notifications
You must be signed in to change notification settings - Fork 111
/
runtime_proxy.go
152 lines (136 loc) · 5.75 KB
/
runtime_proxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package server
import (
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/rilldata/rill/admin/server/auth"
"github.com/rilldata/rill/runtime/pkg/httputil"
runtimeauth "github.com/rilldata/rill/runtime/server/auth"
)
// runtimeProxyForOrgAndProject proxies a request to the runtime service for a specific project.
// This provides a way to directly query a project's runtime on a stable URL without needing to call GetProject or GetDeploymentCredentials to discover the runtime URL.
// If the request is made using an Authorization header or cookie recognized by the admin service,
// the proxied request is made with a newly minted JWT similar to the one that could be obtained by calling GetProject.
// If the Authorization header of the request is not recognized by the admin service, it is proxied through to the runtime service.
func (s *Server) runtimeProxyForOrgAndProject(w http.ResponseWriter, r *http.Request) error {
// Get args from URL path components
org := r.PathValue("org")
project := r.PathValue("project")
proxyPath := r.PathValue("path")
proxyRawQuery := r.URL.RawQuery
// Find the production deployment for the project we're proxying to
proj, err := s.admin.DB.FindProjectByName(r.Context(), org, project)
if err != nil {
return httputil.Error(http.StatusBadRequest, err)
}
if proj.ProdDeploymentID == nil {
return httputil.Errorf(http.StatusBadRequest, "no prod deployment for project")
}
depl, err := s.admin.DB.FindDeployment(r.Context(), *proj.ProdDeploymentID)
if err != nil {
return httputil.Error(http.StatusBadRequest, err)
}
// Get or issue a JWT to use for the proxied request.
var jwt string
claims := auth.GetClaims(r.Context())
switch claims.OwnerType() {
case auth.OwnerTypeAnon:
// If the client is not authenticated with the admin service, we just proxy the contents of the Authorization header to the runtime (if any).
// Note that the authorization middleware for this handler is set to be "lenient",
// which means it will still invoke this handler even if the Authorization header contains a token that is not valid for the admin service.
authorizationHeader := r.Header.Get("Authorization")
if len(authorizationHeader) >= 6 && strings.EqualFold(authorizationHeader[0:6], "bearer") {
jwt = strings.TrimSpace(authorizationHeader[6:])
}
case auth.OwnerTypeUser, auth.OwnerTypeService:
// If the client is authenticated with the admin service, we issue a new ephemeral runtime JWT.
// The JWT should have the same permissions/configuration as one they would get by calling AdminService.GetProject.
permissions := claims.ProjectPermissions(r.Context(), proj.OrganizationID, depl.ProjectID)
if !permissions.ReadProd {
return httputil.Errorf(http.StatusForbidden, "does not have permission to access the production deployment")
}
var attr map[string]any
if claims.OwnerType() == auth.OwnerTypeUser {
attr, err = s.jwtAttributesForUser(r.Context(), claims.OwnerID(), proj.OrganizationID, permissions)
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
}
jwt, err = s.issuer.NewToken(runtimeauth.TokenOptions{
AudienceURL: depl.RuntimeAudience,
Subject: claims.OwnerID(),
TTL: runtimeAccessTokenDefaultTTL,
InstancePermissions: map[string][]runtimeauth.Permission{
depl.RuntimeInstanceID: {
// TODO: Remove ReadProfiling and ReadRepo (may require frontend changes)
runtimeauth.ReadObjects,
runtimeauth.ReadMetrics,
runtimeauth.ReadProfiling,
runtimeauth.ReadRepo,
runtimeauth.ReadAPI,
},
},
Attributes: attr,
})
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
default:
return httputil.Errorf(http.StatusBadRequest, "runtime proxy not available for owner type %q", claims.OwnerType())
}
// Track usage of the deployment
s.admin.Used.Deployment(depl.ID)
// Determine runtime host.
// NOTE: In production, the runtime host serves both the HTTP and gRPC servers.
// But in development, the two are presently on different ports, and depl.RuntimeHost is that of the gRPC server.
// Until we get both servers on the same port in development, this hack rewrites the runtime host to the HTTP server.
runtimeHost := depl.RuntimeHost
if strings.HasPrefix(runtimeHost, "http://localhost:") {
runtimeHost = os.Getenv("RILL_RUNTIME_AUTH_AUDIENCE_URL")
if runtimeHost == "" {
runtimeHost = "http://localhost:8081"
}
}
// Create the URL to proxy to by prepending `/v1/instances/{instanceID}` to the proxy path.
proxyURL, err := url.Parse(runtimeHost)
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
proxyURL = proxyURL.JoinPath("/v1/instances", depl.RuntimeInstanceID, proxyPath)
proxyURL.RawQuery = proxyRawQuery
// Create the proxied request.
req, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL.String(), r.Body)
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
for k, v := range r.Header {
req.Header.Add(k, v[0])
}
// Override the authorization header with the JWT (note use of Set instead of Add).
if jwt != "" {
req.Header.Set("Authorization", "Bearer "+jwt)
} else {
req.Header.Del("Authorization")
}
// Send the proxied request using http.DefaultClient. The default client automatically handles caching/pooling of TCP connections.
res, err := http.DefaultClient.Do(req)
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
defer res.Body.Close()
// Copy the proxied response to the original response writer
outHeader := w.Header()
for k, v := range res.Header {
for _, vv := range v {
outHeader.Add(k, vv)
}
}
w.WriteHeader(res.StatusCode)
_, err = io.Copy(w, res.Body)
if err != nil {
return httputil.Error(http.StatusInternalServerError, err)
}
return nil
}