-
Notifications
You must be signed in to change notification settings - Fork 460
feat(docker-reverse-proxy): gate access token auth behind feature flag #3242
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,14 +14,16 @@ | |
| "github.com/e2b-dev/infra/packages/db/pkg/pool" | ||
| "github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/cache" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/consts" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/featureflags" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/utils" | ||
| ) | ||
|
|
||
| type APIStore struct { | ||
| db *client.Client | ||
| authDb *authdb.Client | ||
| AuthCache *cache.AuthCache | ||
| proxy *httputil.ReverseProxy | ||
| db *client.Client | ||
| authDb *authdb.Client | ||
| AuthCache *cache.AuthCache | ||
| proxy *httputil.ReverseProxy | ||
| featureFlags *featureflags.Client | ||
| } | ||
|
|
||
| func NewStore(ctx context.Context) *APIStore { | ||
|
|
@@ -38,6 +40,11 @@ | |
| log.Fatal(err) | ||
| } | ||
|
|
||
| featureFlags, err := featureflags.NewClient() | ||
| if err != nil { | ||
| log.Fatal(err) | ||
| } | ||
|
Check failure on line 46 in packages/docker-reverse-proxy/internal/handlers/store.go
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LaunchDarkly unset in deploymentHigh Severity
Reviewed by Cursor Bugbot for commit a5ba40d. Configure here.
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Nomad job for docker-reverse-proxy is missing Extended reasoning...What breaksThis PR wires a LaunchDarkly-backed gate into Code path
Step-by-step proof
Why existing code doesn't prevent it
FixOne-line addition to docker_reverse_proxy_env_vars = merge({
POSTGRES_CONNECTION_STRING = ...
# ...
DOMAIN_NAME = var.domain_name
LAUNCH_DARKLY_API_KEY = trimspace(data.google_secret_manager_secret_version.launch_darkly_api_key.secret_data)
}, var.docker_reverse_proxy_env_vars)Also worth auditing any other deployment providers (AWS self-host) for the same omission, since this is a new dependency for docker-reverse-proxy. |
||
|
|
||
| targetUrl := &url.URL{ | ||
| Scheme: "https", | ||
| Host: fmt.Sprintf("%s-docker.pkg.dev", consts.GCPRegion), | ||
|
|
@@ -57,10 +64,11 @@ | |
| } | ||
|
|
||
| return &APIStore{ | ||
| db: database, | ||
| authDb: authDatabase, | ||
| AuthCache: authCache, | ||
| proxy: proxy, | ||
| db: database, | ||
| authDb: authDatabase, | ||
| AuthCache: authCache, | ||
| proxy: proxy, | ||
| featureFlags: featureFlags, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import ( | |
|
|
||
| "github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/auth" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/consts" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/featureflags" | ||
| ) | ||
|
|
||
| type DockerToken struct { | ||
|
|
@@ -39,7 +40,8 @@ func (a *APIStore) GetToken(w http.ResponseWriter, r *http.Request) error { | |
| return fmt.Errorf("error while extracting access token: %w", err) | ||
| } | ||
|
|
||
| if !auth.ValidateAccessToken(ctx, a.authDb, accessToken) { | ||
| userID, ok := auth.ValidateAccessToken(ctx, a.authDb, accessToken) | ||
| if !ok { | ||
| log.Printf("Invalid access token: '%s'\n", accessToken) | ||
|
|
||
| w.WriteHeader(http.StatusForbidden) | ||
|
|
@@ -48,6 +50,15 @@ func (a *APIStore) GetToken(w http.ResponseWriter, r *http.Request) error { | |
| return errors.New("invalid access token") | ||
| } | ||
|
|
||
| // Access token acceptance is gated after validation so the flag can be | ||
| // rolled out per-user via LD targeting during the deprecation cutover. | ||
| if a.featureFlags.BoolFlag(ctx, featureflags.DisableE2BAccessTokenAuthFlag, featureflags.UserContext(userID.String())) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When using this flag for the deprecation cutover, this check only blocks new Useful? React with 👍 / 👎. |
||
| w.WriteHeader(http.StatusForbidden) | ||
| w.Write([]byte("E2B_ACCESS_TOKEN is deprecated and no longer accepted. Use an API key (E2B_API_KEY) instead. See https://e2b.dev/docs/migration/access-token-deprecation")) | ||
|
|
||
| return errors.New("access token authentication is disabled") | ||
| } | ||
|
|
||
| scope := r.URL.Query().Get("scope") | ||
| hasScope := scope != "" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "fmt" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries" | ||
| "github.com/e2b-dev/infra/packages/db/pkg/testutils" | ||
| "github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/cache" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/featureflags" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/keys" | ||
| ) | ||
|
|
||
| func newTokenTestStore(t *testing.T, accessTokenAuthDisabled bool) (*APIStore, keys.Key) { | ||
| t.Helper() | ||
|
|
||
| td := ldtestdata.DataSource() | ||
| td.Update(td.Flag(featureflags.DisableE2BAccessTokenAuthFlag.Key()).VariationForAll(accessTokenAuthDisabled)) | ||
| ff, err := featureflags.NewClientWithDatasource(td) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = ff.Close(t.Context()) }) | ||
|
|
||
| db := testutils.SetupDatabase(t) | ||
|
|
||
| accessToken, err := keys.GenerateKey(keys.AccessTokenPrefix) | ||
| require.NoError(t, err) | ||
|
|
||
| userID := uuid.New() | ||
| require.NoError(t, db.AuthDB.Write.UpsertPublicUser(t.Context(), userID)) | ||
|
|
||
| _, err = db.AuthDB.Write.CreateAccessToken(t.Context(), authqueries.CreateAccessTokenParams{ | ||
| ID: uuid.New(), | ||
| UserID: userID, | ||
| AccessTokenHash: accessToken.HashedValue, | ||
| AccessTokenPrefix: accessToken.Masked.Prefix, | ||
| AccessTokenLength: int32(accessToken.Masked.ValueLength), | ||
| AccessTokenMaskPrefix: accessToken.Masked.MaskedValuePrefix, | ||
| AccessTokenMaskSuffix: accessToken.Masked.MaskedValueSuffix, | ||
| Name: "Test token", | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| return &APIStore{ | ||
| db: db.SqlcClient, | ||
| authDb: db.AuthDB, | ||
| AuthCache: cache.New(), | ||
| featureFlags: ff, | ||
| }, accessToken | ||
| } | ||
|
|
||
| func newTokenRequest(t *testing.T, rawAccessToken string) *http.Request { | ||
| t.Helper() | ||
|
|
||
| req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/v2/token", nil) | ||
| loginInfo := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "_e2b_access_token:%s", rawAccessToken)) | ||
| req.Header.Set("Authorization", "Basic "+loginInfo) | ||
|
|
||
| return req | ||
| } | ||
|
|
||
| func TestGetTokenAcceptsAccessTokenWhenAuthEnabled(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| store, accessToken := newTokenTestStore(t, false) | ||
|
|
||
| recorder := httptest.NewRecorder() | ||
| err := store.GetToken(recorder, newTokenRequest(t, accessToken.PrefixedRawValue)) | ||
|
|
||
| require.NoError(t, err) | ||
| require.Equal(t, http.StatusOK, recorder.Code) | ||
| require.Contains(t, recorder.Body.String(), "token") | ||
| } | ||
|
|
||
| func TestGetTokenRejectsAccessTokenWhenAuthDisabled(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| store, accessToken := newTokenTestStore(t, true) | ||
|
|
||
| recorder := httptest.NewRecorder() | ||
| err := store.GetToken(recorder, newTokenRequest(t, accessToken.PrefixedRawValue)) | ||
|
|
||
| require.Error(t, err) | ||
| require.Equal(t, http.StatusForbidden, recorder.Code) | ||
| require.Contains(t, recorder.Body.String(), "E2B_API_KEY") | ||
| require.Contains(t, recorder.Body.String(), "https://e2b.dev/docs/migration/access-token-deprecation") | ||
| } | ||
|
|
||
| func TestGetTokenRejectsInvalidAccessTokenRegardlessOfFlag(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| store, _ := newTokenTestStore(t, true) | ||
|
|
||
| recorder := httptest.NewRecorder() | ||
| err := store.GetToken(recorder, newTokenRequest(t, keys.AccessTokenPrefix+"invalid")) | ||
|
|
||
| require.Error(t, err) | ||
| require.Equal(t, http.StatusForbidden, recorder.Code) | ||
| require.Contains(t, recorder.Body.String(), "invalid access token") | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the GCP Nomad config I checked,
docker_reverse_proxy_env_varsonly passes the Postgres/Google/GCP/domain values (iac/provider-gcp/main.tf:188-195), whileNewClient()falls back to the offline datasource wheneverLAUNCH_DARKLY_API_KEYis empty (packages/shared/pkg/featureflags/client.go:58-61). Since this commit creates the LD client here but does not wire that env var into the docker-reverse-proxy job, deployed proxy instances will always evaluatedisable-e2b-access-token-authas its defaultfalseand will continue accepting deprecated access-token logins even when the flag is enabled.Useful? React with 👍 / 👎.