Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions packages/dashboard-api/internal/cfg/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cfg
import (
"errors"
"reflect"
"strings"

"github.com/caarlos0/env/v11"

Expand All @@ -30,7 +29,6 @@ type Config struct {

OrySDKURL string `env:"ORY_SDK_URL"`
OryProjectAPIToken string `env:"ORY_PROJECT_API_TOKEN,unset"`
OryIssuerURL string `env:"ORY_ISSUER_URL"`

DomainName string `env:"DOMAIN_NAME" envDefault:""`
}
Expand All @@ -41,8 +39,6 @@ const (
FailureConditionMissingRedisConnection FailureCondition = "missing_redis_connection"
FailureConditionMissingOrySDKURL FailureCondition = "missing_ory_sdk_url"
FailureConditionMissingOryProjectToken FailureCondition = "missing_ory_project_api_token"
FailureConditionMissingOryIssuerURL FailureCondition = "missing_ory_issuer_url"
FailureConditionOryIssuerURLMismatch FailureCondition = "ory_issuer_url_mismatch"
)

type FailureError struct {
Expand Down Expand Up @@ -107,22 +103,5 @@ func validateOryConfig(config *Config) error {
return newFailureError(FailureConditionMissingOryProjectToken, "ORY_PROJECT_API_TOKEN is required")
}

if config.OryIssuerURL == "" && len(config.AuthProvider.JWT) == 1 {
Comment thread
cursor[bot] marked this conversation as resolved.
config.OryIssuerURL = strings.TrimSpace(config.AuthProvider.JWT[0].Issuer.URL)
}
if config.OryIssuerURL == "" {
return newFailureError(FailureConditionMissingOryIssuerURL, "ORY_ISSUER_URL is required")
}

if len(config.AuthProvider.JWT) > 0 {
for _, jwt := range config.AuthProvider.JWT {
if strings.TrimSpace(jwt.Issuer.URL) == config.OryIssuerURL {
return nil
}
}

return newFailureError(FailureConditionOryIssuerURLMismatch, "ORY_ISSUER_URL does not match any AUTH_PROVIDER_CONFIG.jwt[].issuer.url; identities stored at bootstrap would be invisible to the Ory profile provider")
}

return nil
}
63 changes: 4 additions & 59 deletions packages/dashboard-api/internal/cfg/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ func setBaseEnv(t *testing.T) {
t.Setenv("REDIS_URL", "redis://example")
t.Setenv("ORY_SDK_URL", "https://tenant.projects.oryapis.com")
t.Setenv("ORY_PROJECT_API_TOKEN", "pat")
t.Setenv("ORY_ISSUER_URL", "https://auth.example.com")
}

func TestParseAuthProviderConfig(t *testing.T) {
Expand Down Expand Up @@ -51,27 +50,18 @@ func TestParseRequiresOryEnv(t *testing.T) {
name string
sdkURL string
token string
issuer string
wantErrSubstr string
}{
{
name: "without sdk url errors",
token: "pat",
issuer: "https://auth.example.com",
wantErrSubstr: "ORY_SDK_URL",
},
{
name: "without token errors",
sdkURL: "https://tenant.projects.oryapis.com",
issuer: "https://auth.example.com",
wantErrSubstr: "ORY_PROJECT_API_TOKEN",
},
{
name: "without issuer errors",
sdkURL: "https://tenant.projects.oryapis.com",
token: "pat",
wantErrSubstr: "ORY_ISSUER_URL",
},
}

for _, tt := range tests {
Expand All @@ -81,7 +71,6 @@ func TestParseRequiresOryEnv(t *testing.T) {
t.Setenv("REDIS_URL", "redis://example")
t.Setenv("ORY_SDK_URL", tt.sdkURL)
t.Setenv("ORY_PROJECT_API_TOKEN", tt.token)
t.Setenv("ORY_ISSUER_URL", tt.issuer)

_, err := Parse()
require.Error(t, err)
Expand All @@ -90,18 +79,17 @@ func TestParseRequiresOryEnv(t *testing.T) {
}
}

func TestParseOryHappyPathIsIndependentOfAuthProvider(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used with t.Parallel.
func TestParseOryHappyPath(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used with t.Parallel.
setBaseEnv(t)

config, err := Parse()
require.NoError(t, err)
require.Equal(t, "https://tenant.projects.oryapis.com", config.OrySDKURL)
require.Equal(t, "pat", config.OryProjectAPIToken)
require.Equal(t, "https://auth.example.com", config.OryIssuerURL)
require.Empty(t, config.AuthProvider.JWT)
}

func TestParseOryIssuerDefaultsFromSingleAuthProviderJWT(t *testing.T) {
func TestParseOryWithAuthProviderConfig(t *testing.T) {
t.Setenv("POSTGRES_CONNECTION_STRING", "postgres://example")
t.Setenv("ADMIN_TOKEN", "admin-token")
t.Setenv("REDIS_URL", "redis://example")
Expand All @@ -115,21 +103,8 @@ func TestParseOryIssuerDefaultsFromSingleAuthProviderJWT(t *testing.T) {

config, err := Parse()
require.NoError(t, err)
require.Equal(t, "https://auth.mycompany.com", config.OryIssuerURL)
}

func TestParseOryIssuerRejectsMismatchAgainstAuthProvider(t *testing.T) {
setBaseEnv(t)
t.Setenv("ORY_ISSUER_URL", "https://tenant.projects.oryapis.com")
t.Setenv("AUTH_PROVIDER_CONFIG", `{
"jwt": [
{"issuer": {"url": "https://auth.mycompany.com", "audiences": ["dashboard-api"]}}
]
}`)

_, err := Parse()
require.Error(t, err)
require.Contains(t, err.Error(), "does not match any AUTH_PROVIDER_CONFIG")
require.Len(t, config.AuthProvider.JWT, 1)
require.Equal(t, "https://auth.mycompany.com", config.AuthProvider.JWT[0].Issuer.URL)
}

func TestParseFailureCondition(t *testing.T) {
Expand All @@ -153,7 +128,6 @@ func TestParseFailureCondition(t *testing.T) {
"ADMIN_TOKEN": "admin-token",
"REDIS_URL": "redis://example",
"ORY_PROJECT_API_TOKEN": "pat",
"ORY_ISSUER_URL": "https://auth.example.com",
},
want: FailureConditionMissingOrySDKURL,
},
Expand All @@ -164,38 +138,9 @@ func TestParseFailureCondition(t *testing.T) {
"ADMIN_TOKEN": "admin-token",
"REDIS_URL": "redis://example",
"ORY_SDK_URL": "https://tenant.projects.oryapis.com",
"ORY_ISSUER_URL": "https://auth.example.com",
},
want: FailureConditionMissingOryProjectToken,
},
{
name: "missing ory issuer url",
env: map[string]string{
"POSTGRES_CONNECTION_STRING": "postgres://example",
"ADMIN_TOKEN": "admin-token",
"REDIS_URL": "redis://example",
"ORY_SDK_URL": "https://tenant.projects.oryapis.com",
"ORY_PROJECT_API_TOKEN": "pat",
},
want: FailureConditionMissingOryIssuerURL,
},
{
name: "ory issuer url mismatch",
env: map[string]string{
"POSTGRES_CONNECTION_STRING": "postgres://example",
"ADMIN_TOKEN": "admin-token",
"REDIS_URL": "redis://example",
"ORY_SDK_URL": "https://tenant.projects.oryapis.com",
"ORY_PROJECT_API_TOKEN": "pat",
"ORY_ISSUER_URL": "https://tenant.projects.oryapis.com",
"AUTH_PROVIDER_CONFIG": `{
"jwt": [
{"issuer": {"url": "https://auth.example.com", "audiences": ["dashboard-api"]}}
]
}`,
},
want: FailureConditionOryIssuerURLMismatch,
},
}

for _, tt := range tests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"go.uber.org/zap"

"github.com/e2b-dev/infra/packages/dashboard-api/internal/api"
"github.com/e2b-dev/infra/packages/dashboard-api/internal/userprofile"
"github.com/e2b-dev/infra/packages/dashboard-api/internal/identity"
"github.com/e2b-dev/infra/packages/shared/pkg/ginutils"
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
)
Expand Down Expand Up @@ -41,7 +41,7 @@ func (s *APIStore) PostAdminUserProfilesResolve(c *gin.Context) {
seen[userID] = struct{}{}
}

profiles, err := s.userProfiles.GetProfilesByUserID(ctx, body.UserIds)
profiles, err := s.identityService.ProfilesByUserID(ctx, body.UserIds)
if err != nil {
logger.L().Error(ctx, "failed to resolve auth provider profiles", zap.Error(err))
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to resolve auth provider profiles")
Expand All @@ -64,7 +64,7 @@ func (s *APIStore) PostAdminUserProfilesByEmail(c *gin.Context) {
return
}

profiles, err := s.userProfiles.FindProfilesByEmail(ctx, string(body.Email))
profiles, err := s.identityService.FindProfilesByEmail(ctx, string(body.Email))
if err != nil {
logger.L().Error(ctx, "failed to look up auth provider profiles by email", zap.Error(err))
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to look up auth provider profiles")
Expand All @@ -79,7 +79,7 @@ func (s *APIStore) PostAdminUserProfilesByEmail(c *gin.Context) {

func (s *APIStore) GetAdminUserProfilesUserId(c *gin.Context, userId api.UserId) {
ctx := c.Request.Context()
profiles, err := s.userProfiles.GetProfilesByUserID(ctx, []uuid.UUID{userId})
profiles, err := s.identityService.ProfilesByUserID(ctx, []uuid.UUID{userId})
if err != nil {
logger.L().Error(ctx, "failed to resolve auth provider profile", zap.Error(err))
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to resolve auth provider profile")
Expand All @@ -92,7 +92,7 @@ func (s *APIStore) GetAdminUserProfilesUserId(c *gin.Context, userId api.UserId)
})
}

func apiProfilesFromMap(userIDs []uuid.UUID, profiles map[uuid.UUID]userprofile.Profile) []api.AdminAuthProviderProfile {
func apiProfilesFromMap(userIDs []uuid.UUID, profiles map[uuid.UUID]identity.Profile) []api.AdminAuthProviderProfile {
result := make([]api.AdminAuthProviderProfile, 0, len(profiles))
seen := make(map[uuid.UUID]struct{}, len(userIDs))

Expand All @@ -113,7 +113,7 @@ func apiProfilesFromMap(userIDs []uuid.UUID, profiles map[uuid.UUID]userprofile.
return result
}

func apiProfilesFromProfiles(profiles []userprofile.Profile) []api.AdminAuthProviderProfile {
func apiProfilesFromProfiles(profiles []identity.Profile) []api.AdminAuthProviderProfile {
result := make([]api.AdminAuthProviderProfile, 0, len(profiles))

for _, profile := range profiles {
Expand All @@ -123,7 +123,7 @@ func apiProfilesFromProfiles(profiles []userprofile.Profile) []api.AdminAuthProv
return result
}

func apiProfileFromProfile(profile userprofile.Profile) api.AdminAuthProviderProfile {
func apiProfileFromProfile(profile identity.Profile) api.AdminAuthProviderProfile {
var email *string
if profile.Email != "" {
email = new(profile.Email)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ func (s *APIStore) PostAdminTeamsBootstrap(c *gin.Context) {
return
}

team, err := s.bootstrapTeam(ctx, name, email)
team, err := s.provisioningService.BootstrapTeam(ctx, name, email)
if err != nil {
s.handleProvisioningError(ctx, c, "provision team", err)
s.sendProvisioningError(ctx, c, "provision team", err)

return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5"

"github.com/e2b-dev/infra/packages/dashboard-api/internal/provisioning"
internalteamprovision "github.com/e2b-dev/infra/packages/dashboard-api/internal/teamprovision"
"github.com/e2b-dev/infra/packages/db/pkg/testutils"
"github.com/e2b-dev/infra/packages/shared/pkg/teamprovision"
Expand All @@ -32,9 +33,10 @@ func TestPostAdminTeamsBootstrapCreatesTeam(t *testing.T) {
}`))
ginCtx.Request.Header.Set("Content-Type", "application/json")

provisioningService := provisioning.New(testDB.AuthDB, nil, sink)

store := &APIStore{
authDB: testDB.AuthDB,
teamProvisionSink: sink,
provisioningService: provisioningService,
}
store.PostAdminTeamsBootstrap(ginCtx)

Expand Down Expand Up @@ -108,9 +110,10 @@ func TestPostAdminTeamsBootstrapRollsBackOnProvisioningFailure(t *testing.T) {
}`))
ginCtx.Request.Header.Set("Content-Type", "application/json")

provisioningService := provisioning.New(testDB.AuthDB, nil, sink)

store := &APIStore{
authDB: testDB.AuthDB,
teamProvisionSink: sink,
provisioningService: provisioningService,
}
store.PostAdminTeamsBootstrap(ginCtx)

Expand Down Expand Up @@ -148,9 +151,10 @@ func TestPostAdminTeamsBootstrapRejectsMissingFields(t *testing.T) {
}`))
ginCtx.Request.Header.Set("Content-Type", "application/json")

provisioningService := provisioning.New(testDB.AuthDB, nil, &fakeTeamProvisionSink{err: errors.New("should not provision")})

store := &APIStore{
authDB: testDB.AuthDB,
teamProvisionSink: &fakeTeamProvisionSink{err: errors.New("should not provision")},
provisioningService: provisioningService,
}
store.PostAdminTeamsBootstrap(ginCtx)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"

"github.com/e2b-dev/infra/packages/dashboard-api/internal/api"
"github.com/e2b-dev/infra/packages/dashboard-api/internal/provisioning"
"github.com/e2b-dev/infra/packages/shared/pkg/ginutils"
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
)
Expand All @@ -35,7 +36,7 @@ func (s *APIStore) PostAdminUsersBootstrap(c *gin.Context) {
return
}

team, err := s.bootstrapOIDCUser(ctx, oidcUserBootstrapInput{
team, err := s.provisioningService.BootstrapOIDCUser(ctx, provisioning.OIDCUserBootstrapInput{
OIDCIssuer: oidcIssuer,
OIDCUserID: oidcUserID,
OIDCUserEmail: oidcUserEmail,
Expand All @@ -44,7 +45,7 @@ func (s *APIStore) PostAdminUsersBootstrap(c *gin.Context) {
SignupUserAgent: strings.TrimSpace(valueOrEmpty(body.SignupUserAgent)),
})
if err != nil {
s.handleProvisioningError(ctx, c, "bootstrap auth provider user", err)
s.sendProvisioningError(ctx, c, "bootstrap auth provider user", err)

return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"go.uber.org/zap"

"github.com/e2b-dev/infra/packages/dashboard-api/internal/api"
"github.com/e2b-dev/infra/packages/dashboard-api/internal/userprofile"
"github.com/e2b-dev/infra/packages/dashboard-api/internal/identity"
"github.com/e2b-dev/infra/packages/db/pkg/dberrors"
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
)
Expand All @@ -21,10 +21,9 @@ const identityDeleteMaxRetries = 3
func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) {
ctx := c.Request.Context()

// Resolve the external identity references while user_identities still exists.
handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId)
handle, err := s.identityService.PrepareDeleteUser(ctx, userId)
if err != nil {
if errors.Is(err, userprofile.ErrUserNotFound) {
if errors.Is(err, identity.ErrUserNotFound) {
s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found or has no identity provider record", userId))

return
Expand All @@ -36,7 +35,6 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) {
return
}

// Delete from public.users (cascades to user_identities via FK).
// Done before the IdP removal so a DB failure does not orphan the identity.
if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil {
if dberrors.IsNotFoundError(err) {
Expand All @@ -58,7 +56,6 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) {
return
}

// Remove the external identity (e.g. Ory) using pre-fetched references.
// Retry since the DB rows are already gone and we must not leave the IdP identity active.
// Use a detached context so a client disconnect does not cancel the cleanup.
cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
Expand Down
Loading
Loading