diff --git a/server/api.go b/server/api.go index 724c4807..202fef36 100644 --- a/server/api.go +++ b/server/api.go @@ -470,6 +470,11 @@ func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq return nil, fmt.Errorf("create connector: %v", err) } + // Make sure we don't reuse stale entries in the cache + if d.server != nil { + d.server.CloseConnector(req.Connector.Id) + } + return &api.CreateConnectorResp{}, nil } @@ -538,6 +543,7 @@ func (d dexAPI) DeleteConnector(ctx context.Context, req *api.DeleteConnectorReq d.logger.Error("api: failed to delete connector", "err", err) return nil, fmt.Errorf("delete connector: %v", err) } + return &api.DeleteConnectorResp{}, nil } diff --git a/server/api_cache_test.go b/server/api_cache_test.go new file mode 100644 index 00000000..64564c46 --- /dev/null +++ b/server/api_cache_test.go @@ -0,0 +1,133 @@ +package server + +import ( + "context" + "encoding/json" + "testing" + + "github.com/dexidp/dex/api/v2" + "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/connector/mock" + "github.com/dexidp/dex/storage/memory" +) + +func TestConnectorCacheInvalidation(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + serv := &Server{ + storage: s, + logger: logger, + connectors: make(map[string]Connector), + } + + apiServer := NewAPI(s, logger, "test", serv) + ctx := context.Background() + + connID := "mock-conn" + + // 1. Create a connector via API + config1 := mock.PasswordConfig{ + Username: "user", + Password: "first-password", + } + config1Bytes, _ := json.Marshal(config1) + + _, err := apiServer.CreateConnector(ctx, &api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connID, + Type: "mockPassword", + Name: "Mock", + Config: config1Bytes, + }, + }) + if err != nil { + t.Fatalf("failed to create connector: %v", err) + } + + // 2. Load it into server cache + c1, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector: %v", err) + } + + pc1 := c1.Connector.(connector.PasswordConnector) + _, valid, err := pc1.Login(ctx, connector.Scopes{}, "user", "first-password") + if err != nil || !valid { + t.Fatalf("failed to login with first password: %v", err) + } + + // 3. Delete it via API + _, err = apiServer.DeleteConnector(ctx, &api.DeleteConnectorReq{Id: connID}) + if err != nil { + t.Fatalf("failed to delete connector: %v", err) + } + + // 4. Create it again with different password + config2 := mock.PasswordConfig{ + Username: "user", + Password: "second-password", + } + config2Bytes, _ := json.Marshal(config2) + + _, err = apiServer.CreateConnector(ctx, &api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connID, + Type: "mockPassword", + Name: "Mock", + Config: config2Bytes, + }, + }) + if err != nil { + t.Fatalf("failed to create connector: %v", err) + } + + // 5. Load it again + c2, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector second time: %v", err) + } + + pc2 := c2.Connector.(connector.PasswordConnector) + + // If the fix works, it should now use the second password. + _, valid2, err := pc2.Login(ctx, connector.Scopes{}, "user", "second-password") + if err != nil || !valid2 { + t.Errorf("failed to login with second password, cache might still be stale") + } + + _, valid1, _ := pc2.Login(ctx, connector.Scopes{}, "user", "first-password") + if valid1 { + t.Errorf("unexpectedly logged in with first password, cache is definitely stale") + } + + // 6. Update it via API with a third password + config3 := mock.PasswordConfig{ + Username: "user", + Password: "third-password", + } + config3Bytes, _ := json.Marshal(config3) + + _, err = apiServer.UpdateConnector(ctx, &api.UpdateConnectorReq{ + Id: connID, + NewConfig: config3Bytes, + }) + if err != nil { + t.Fatalf("failed to update connector: %v", err) + } + + // 7. Load it again + c3, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector third time: %v", err) + } + + pc3 := c3.Connector.(connector.PasswordConnector) + + _, valid3, err := pc3.Login(ctx, connector.Scopes{}, "user", "third-password") + if err != nil || !valid3 { + t.Errorf("failed to login with third password, UpdateConnector might be missing cache invalidation") + } +} diff --git a/server/server.go b/server/server.go index e923e3e0..ea87aa1c 100644 --- a/server/server.go +++ b/server/server.go @@ -745,6 +745,13 @@ func (s *Server) OpenConnector(conn storage.Connector) (Connector, error) { return connector, nil } +// CloseConnector removes the connector from the server's in-memory map. +func (s *Server) CloseConnector(id string) { + s.mu.Lock() + delete(s.connectors, id) + s.mu.Unlock() +} + // getConnector retrieves the connector object with the given id from the storage // and updates the connector list for server if necessary. func (s *Server) getConnector(ctx context.Context, id string) (Connector, error) {