mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
gRPC Connectors API (#3245)
Signed-off-by: Giovanni Campeol <giovanni.campeol.95@gmail.com> Signed-off-by: Maksim Nabokikh <maksim.nabokikh@flant.com> Co-authored-by: Maksim Nabokikh <maksim.nabokikh@flant.com>
This commit is contained in:
co-authored by
Maksim Nabokikh
parent
2669b61daf
commit
b07e1bc9f1
+834
-182
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,56 @@ message ListPasswordResp {
|
||||
repeated Password passwords = 1;
|
||||
}
|
||||
|
||||
// Connector is a strategy used by Dex for authenticating a user against another identity provider
|
||||
message Connector {
|
||||
string id = 1;
|
||||
string type = 2;
|
||||
string name = 3;
|
||||
bytes config = 4;
|
||||
}
|
||||
|
||||
// CreateConnectorReq is a request to make a connector.
|
||||
message CreateConnectorReq {
|
||||
Connector connector = 1;
|
||||
}
|
||||
|
||||
// CreateConnectorResp returns the response from creating a connector.
|
||||
message CreateConnectorResp {
|
||||
bool already_exists = 1;
|
||||
}
|
||||
|
||||
// UpdateConnectorReq is a request to modify an existing connector.
|
||||
message UpdateConnectorReq {
|
||||
// The id used to lookup the connector. This field cannot be modified
|
||||
string id = 1;
|
||||
string new_type = 2;
|
||||
string new_name = 3;
|
||||
bytes new_config = 4;
|
||||
}
|
||||
|
||||
// UpdateConnectorResp returns the response from modifying an existing connector.
|
||||
message UpdateConnectorResp {
|
||||
bool not_found = 1;
|
||||
}
|
||||
|
||||
// DeleteConnectorReq is a request to delete a connector.
|
||||
message DeleteConnectorReq {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// DeleteConnectorResp returns the response from deleting a connector.
|
||||
message DeleteConnectorResp {
|
||||
bool not_found = 1;
|
||||
}
|
||||
|
||||
// ListConnectorReq is a request to enumerate connectors.
|
||||
message ListConnectorReq {}
|
||||
|
||||
// ListConnectorResp returns a list of connectors.
|
||||
message ListConnectorResp {
|
||||
repeated Connector connectors = 1;
|
||||
}
|
||||
|
||||
// VersionReq is a request to fetch version info.
|
||||
message VersionReq {}
|
||||
|
||||
@@ -189,6 +239,14 @@ service Dex {
|
||||
rpc DeletePassword(DeletePasswordReq) returns (DeletePasswordResp) {};
|
||||
// ListPassword lists all password entries.
|
||||
rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {};
|
||||
// CreateConnector creates a connector.
|
||||
rpc CreateConnector(CreateConnectorReq) returns (CreateConnectorResp) {};
|
||||
// UpdateConnector modifies existing connector.
|
||||
rpc UpdateConnector(UpdateConnectorReq) returns (UpdateConnectorResp) {};
|
||||
// DeleteConnector deletes the connector.
|
||||
rpc DeleteConnector(DeleteConnectorReq) returns (DeleteConnectorResp) {};
|
||||
// ListConnectors lists all connector entries.
|
||||
rpc ListConnectors(ListConnectorReq) returns (ListConnectorResp) {};
|
||||
// GetVersion returns version information of the server.
|
||||
rpc GetVersion(VersionReq) returns (VersionResp) {};
|
||||
// ListRefresh lists all the refresh token entries for a particular user.
|
||||
|
||||
+168
-12
@@ -19,18 +19,22 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
const (
|
||||
Dex_GetClient_FullMethodName = "/api.Dex/GetClient"
|
||||
Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient"
|
||||
Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient"
|
||||
Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient"
|
||||
Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword"
|
||||
Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword"
|
||||
Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword"
|
||||
Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords"
|
||||
Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion"
|
||||
Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh"
|
||||
Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh"
|
||||
Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword"
|
||||
Dex_GetClient_FullMethodName = "/api.Dex/GetClient"
|
||||
Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient"
|
||||
Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient"
|
||||
Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient"
|
||||
Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword"
|
||||
Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword"
|
||||
Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword"
|
||||
Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords"
|
||||
Dex_CreateConnector_FullMethodName = "/api.Dex/CreateConnector"
|
||||
Dex_UpdateConnector_FullMethodName = "/api.Dex/UpdateConnector"
|
||||
Dex_DeleteConnector_FullMethodName = "/api.Dex/DeleteConnector"
|
||||
Dex_ListConnectors_FullMethodName = "/api.Dex/ListConnectors"
|
||||
Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion"
|
||||
Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh"
|
||||
Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh"
|
||||
Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword"
|
||||
)
|
||||
|
||||
// DexClient is the client API for Dex service.
|
||||
@@ -53,6 +57,14 @@ type DexClient interface {
|
||||
DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error)
|
||||
// ListPassword lists all password entries.
|
||||
ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error)
|
||||
// CreateConnector creates a connector.
|
||||
CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error)
|
||||
// UpdateConnector modifies existing connector.
|
||||
UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error)
|
||||
// DeleteConnector deletes the connector.
|
||||
DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error)
|
||||
// ListConnectors lists all connector entries.
|
||||
ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error)
|
||||
// GetVersion returns version information of the server.
|
||||
GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error)
|
||||
// ListRefresh lists all the refresh token entries for a particular user.
|
||||
@@ -145,6 +157,42 @@ func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dexClient) CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error) {
|
||||
out := new(CreateConnectorResp)
|
||||
err := c.cc.Invoke(ctx, Dex_CreateConnector_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dexClient) UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error) {
|
||||
out := new(UpdateConnectorResp)
|
||||
err := c.cc.Invoke(ctx, Dex_UpdateConnector_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dexClient) DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error) {
|
||||
out := new(DeleteConnectorResp)
|
||||
err := c.cc.Invoke(ctx, Dex_DeleteConnector_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dexClient) ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error) {
|
||||
out := new(ListConnectorResp)
|
||||
err := c.cc.Invoke(ctx, Dex_ListConnectors_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) {
|
||||
out := new(VersionResp)
|
||||
err := c.cc.Invoke(ctx, Dex_GetVersion_FullMethodName, in, out, opts...)
|
||||
@@ -201,6 +249,14 @@ type DexServer interface {
|
||||
DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error)
|
||||
// ListPassword lists all password entries.
|
||||
ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error)
|
||||
// CreateConnector creates a connector.
|
||||
CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error)
|
||||
// UpdateConnector modifies existing connector.
|
||||
UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error)
|
||||
// DeleteConnector deletes the connector.
|
||||
DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error)
|
||||
// ListConnectors lists all connector entries.
|
||||
ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error)
|
||||
// GetVersion returns version information of the server.
|
||||
GetVersion(context.Context, *VersionReq) (*VersionResp, error)
|
||||
// ListRefresh lists all the refresh token entries for a particular user.
|
||||
@@ -242,6 +298,18 @@ func (UnimplementedDexServer) DeletePassword(context.Context, *DeletePasswordReq
|
||||
func (UnimplementedDexServer) ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPasswords not implemented")
|
||||
}
|
||||
func (UnimplementedDexServer) CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateConnector not implemented")
|
||||
}
|
||||
func (UnimplementedDexServer) UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateConnector not implemented")
|
||||
}
|
||||
func (UnimplementedDexServer) DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteConnector not implemented")
|
||||
}
|
||||
func (UnimplementedDexServer) ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListConnectors not implemented")
|
||||
}
|
||||
func (UnimplementedDexServer) GetVersion(context.Context, *VersionReq) (*VersionResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
|
||||
}
|
||||
@@ -411,6 +479,78 @@ func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Dex_CreateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateConnectorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DexServer).CreateConnector(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Dex_CreateConnector_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DexServer).CreateConnector(ctx, req.(*CreateConnectorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Dex_UpdateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateConnectorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DexServer).UpdateConnector(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Dex_UpdateConnector_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DexServer).UpdateConnector(ctx, req.(*UpdateConnectorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Dex_DeleteConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeleteConnectorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DexServer).DeleteConnector(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Dex_DeleteConnector_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DexServer).DeleteConnector(ctx, req.(*DeleteConnectorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Dex_ListConnectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListConnectorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DexServer).ListConnectors(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Dex_ListConnectors_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DexServer).ListConnectors(ctx, req.(*ListConnectorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(VersionReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -522,6 +662,22 @@ var Dex_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListPasswords",
|
||||
Handler: _Dex_ListPasswords_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateConnector",
|
||||
Handler: _Dex_CreateConnector_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateConnector",
|
||||
Handler: _Dex_UpdateConnector_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeleteConnector",
|
||||
Handler: _Dex_DeleteConnector_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListConnectors",
|
||||
Handler: _Dex_ListConnectors_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetVersion",
|
||||
Handler: _Dex_GetVersion_Handler,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -50,10 +51,22 @@ type Config struct {
|
||||
// querying the storage. Cannot be specified without enabling a passwords
|
||||
// database.
|
||||
StaticPasswords []password `json:"staticPasswords"`
|
||||
|
||||
// AdditionalFeature allow the extension of Dex functionalities
|
||||
AdditionalFeatures []server.AdditionalFeature `json:"additionalFeatures"`
|
||||
}
|
||||
|
||||
// Parse the configuration
|
||||
func (c *Config) Parse() {
|
||||
if c.AdditionalFeatures == nil {
|
||||
c.AdditionalFeatures = []server.AdditionalFeature{}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the configuration
|
||||
func (c Config) Validate() error {
|
||||
invalidFeatures := c.findInvalidAdditionalFeatures()
|
||||
|
||||
// Fast checks. Perform these first for a more responsive CLI.
|
||||
checks := []struct {
|
||||
bad bool
|
||||
@@ -72,6 +85,7 @@ func (c Config) Validate() error {
|
||||
{c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
|
||||
{(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"},
|
||||
{c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"},
|
||||
{len(invalidFeatures) > 0, fmt.Sprintf("invalid additionalFeatures supplied: %v. Valid entries: %s", invalidFeatures, server.ValidAdditionalFeatures)},
|
||||
{c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion != "1.2" && c.GRPC.TLSMinVersion != "1.3", "supported TLS versions are: 1.2, 1.3"},
|
||||
{c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMaxVersion != "1.2" && c.GRPC.TLSMaxVersion != "1.3", "supported TLS versions are: 1.2, 1.3"},
|
||||
{c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion > c.GRPC.TLSMaxVersion, "TLSMinVersion greater than TLSMaxVersion"},
|
||||
@@ -90,6 +104,22 @@ func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// findInvalidAdditionalFeatures returns additional features that are not considered valid
|
||||
func (c Config) findInvalidAdditionalFeatures() []server.AdditionalFeature {
|
||||
if c.AdditionalFeatures == nil {
|
||||
return []server.AdditionalFeature{}
|
||||
}
|
||||
|
||||
badFeatures := []server.AdditionalFeature{}
|
||||
for _, feature := range c.AdditionalFeatures {
|
||||
if !slices.Contains(server.ValidAdditionalFeatures, feature) {
|
||||
badFeatures = append(badFeatures, feature)
|
||||
}
|
||||
}
|
||||
|
||||
return badFeatures
|
||||
}
|
||||
|
||||
type password storage.Password
|
||||
|
||||
func (p *password) UnmarshalJSON(b []byte) error {
|
||||
|
||||
@@ -37,7 +37,11 @@ func TestValidConfiguration(t *testing.T) {
|
||||
Config: &mock.CallbackConfig{},
|
||||
},
|
||||
},
|
||||
AdditionalFeatures: server.ValidAdditionalFeatures,
|
||||
}
|
||||
|
||||
configuration.Parse()
|
||||
|
||||
if err := configuration.Validate(); err != nil {
|
||||
t.Fatalf("this configuration should have been valid: %v", err)
|
||||
}
|
||||
@@ -45,6 +49,7 @@ func TestValidConfiguration(t *testing.T) {
|
||||
|
||||
func TestInvalidConfiguration(t *testing.T) {
|
||||
configuration := Config{}
|
||||
configuration.Parse()
|
||||
err := configuration.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("this configuration should be invalid")
|
||||
@@ -131,6 +136,10 @@ expiry:
|
||||
logger:
|
||||
level: "debug"
|
||||
format: "json"
|
||||
|
||||
additionalFeatures: [
|
||||
"ConnectorsCRUD"
|
||||
]
|
||||
`)
|
||||
|
||||
want := Config{
|
||||
@@ -223,12 +232,16 @@ logger:
|
||||
Level: slog.LevelDebug,
|
||||
Format: "json",
|
||||
},
|
||||
AdditionalFeatures: server.ValidAdditionalFeatures,
|
||||
}
|
||||
|
||||
var c Config
|
||||
if err := yaml.Unmarshal(rawConfig, &c); err != nil {
|
||||
t.Fatalf("failed to decode config: %v", err)
|
||||
}
|
||||
|
||||
c.Parse()
|
||||
|
||||
if diff := pretty.Compare(c, want); diff != "" {
|
||||
t.Errorf("got!=want: %s", diff)
|
||||
}
|
||||
@@ -436,7 +449,19 @@ logger:
|
||||
if err := yaml.Unmarshal(rawConfig, &c); err != nil {
|
||||
t.Fatalf("failed to decode config: %v", err)
|
||||
}
|
||||
|
||||
c.Parse()
|
||||
|
||||
if diff := pretty.Compare(c, want); diff != "" {
|
||||
t.Errorf("got!=want: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
configuration := Config{}
|
||||
configuration.Parse()
|
||||
|
||||
if configuration.AdditionalFeatures == nil || len(configuration.AdditionalFeatures) != 0 {
|
||||
t.Fatal("AdditionalFeatures should be an empty slice")
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -99,6 +99,7 @@ func runServe(options serveOptions) error {
|
||||
return fmt.Errorf("error parse config file %s: %v", configFile, err)
|
||||
}
|
||||
|
||||
c.Parse()
|
||||
applyConfigOverrides(options, &c)
|
||||
|
||||
logger, err := newLogger(c.Logger.Level, c.Logger.Format)
|
||||
@@ -501,7 +502,7 @@ func runServe(options serveOptions) error {
|
||||
}
|
||||
|
||||
grpcSrv := grpc.NewServer(grpcOptions...)
|
||||
api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version))
|
||||
api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version, c.AdditionalFeatures))
|
||||
|
||||
grpcMetrics.InitializeMetrics(grpcSrv)
|
||||
if c.GRPC.Reflection {
|
||||
|
||||
@@ -162,3 +162,8 @@ staticPasswords:
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
|
||||
# A list of features that extend Dex functionalities
|
||||
# additionalFeatures:
|
||||
# # allows CRUD operations on connectors through the gRPC API
|
||||
# - "ConnectorsCRUD"
|
||||
+142
-4
@@ -2,9 +2,11 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
@@ -29,11 +31,12 @@ const (
|
||||
)
|
||||
|
||||
// NewAPI returns a server which implements the gRPC API interface.
|
||||
func NewAPI(s storage.Storage, logger *slog.Logger, version string) api.DexServer {
|
||||
func NewAPI(s storage.Storage, logger *slog.Logger, version string, additionalFeatures []AdditionalFeature) api.DexServer {
|
||||
return dexAPI{
|
||||
s: s,
|
||||
logger: logger.With("component", "api"),
|
||||
version: version,
|
||||
s: s,
|
||||
logger: logger.With("component", "api"),
|
||||
version: version,
|
||||
additionalFeatures: additionalFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +46,8 @@ type dexAPI struct {
|
||||
s storage.Storage
|
||||
logger *slog.Logger
|
||||
version string
|
||||
|
||||
additionalFeatures []AdditionalFeature
|
||||
}
|
||||
|
||||
func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetClientResp, error) {
|
||||
@@ -385,3 +390,136 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
|
||||
|
||||
return &api.RevokeRefreshResp{}, nil
|
||||
}
|
||||
|
||||
func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq) (*api.CreateConnectorResp, error) {
|
||||
if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
|
||||
return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
|
||||
}
|
||||
|
||||
if req.Connector.Id == "" {
|
||||
return nil, errors.New("no id supplied")
|
||||
}
|
||||
|
||||
if req.Connector.Type == "" {
|
||||
return nil, errors.New("no type supplied")
|
||||
}
|
||||
|
||||
if req.Connector.Name == "" {
|
||||
return nil, errors.New("no name supplied")
|
||||
}
|
||||
|
||||
if len(req.Connector.Config) == 0 {
|
||||
return nil, errors.New("no config supplied")
|
||||
}
|
||||
|
||||
if !json.Valid(req.Connector.Config) {
|
||||
return nil, errors.New("invalid config supplied")
|
||||
}
|
||||
|
||||
c := storage.Connector{
|
||||
ID: req.Connector.Id,
|
||||
Name: req.Connector.Name,
|
||||
Type: req.Connector.Type,
|
||||
Config: req.Connector.Config,
|
||||
}
|
||||
if err := d.s.CreateConnector(ctx, c); err != nil {
|
||||
if err == storage.ErrAlreadyExists {
|
||||
return &api.CreateConnectorResp{AlreadyExists: true}, nil
|
||||
}
|
||||
d.logger.Error("api: failed to create connector", "err", err)
|
||||
return nil, fmt.Errorf("create connector: %v", err)
|
||||
}
|
||||
|
||||
return &api.CreateConnectorResp{}, nil
|
||||
}
|
||||
|
||||
func (d dexAPI) UpdateConnector(ctx context.Context, req *api.UpdateConnectorReq) (*api.UpdateConnectorResp, error) {
|
||||
if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
|
||||
return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
|
||||
}
|
||||
|
||||
if req.Id == "" {
|
||||
return nil, errors.New("no email supplied")
|
||||
}
|
||||
|
||||
if len(req.NewConfig) == 0 && req.NewName == "" && req.NewType == "" {
|
||||
return nil, errors.New("nothing to update")
|
||||
}
|
||||
|
||||
if !json.Valid(req.NewConfig) {
|
||||
return nil, errors.New("invalid config supplied")
|
||||
}
|
||||
|
||||
updater := func(old storage.Connector) (storage.Connector, error) {
|
||||
if req.NewType != "" {
|
||||
old.Type = req.NewType
|
||||
}
|
||||
|
||||
if req.NewName != "" {
|
||||
old.Name = req.NewName
|
||||
}
|
||||
|
||||
if len(req.NewConfig) != 0 {
|
||||
old.Config = req.NewConfig
|
||||
}
|
||||
|
||||
return old, nil
|
||||
}
|
||||
|
||||
if err := d.s.UpdateConnector(req.Id, updater); err != nil {
|
||||
if err == storage.ErrNotFound {
|
||||
return &api.UpdateConnectorResp{NotFound: true}, nil
|
||||
}
|
||||
d.logger.Error("api: failed to update connector", "err", err)
|
||||
return nil, fmt.Errorf("update connector: %v", err)
|
||||
}
|
||||
|
||||
return &api.UpdateConnectorResp{}, nil
|
||||
}
|
||||
|
||||
func (d dexAPI) DeleteConnector(ctx context.Context, req *api.DeleteConnectorReq) (*api.DeleteConnectorResp, error) {
|
||||
if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
|
||||
return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
|
||||
}
|
||||
|
||||
if req.Id == "" {
|
||||
return nil, errors.New("no id supplied")
|
||||
}
|
||||
|
||||
err := d.s.DeleteConnector(req.Id)
|
||||
if err != nil {
|
||||
if err == storage.ErrNotFound {
|
||||
return &api.DeleteConnectorResp{NotFound: true}, nil
|
||||
}
|
||||
d.logger.Error("api: failed to delete connector", "err", err)
|
||||
return nil, fmt.Errorf("delete connector: %v", err)
|
||||
}
|
||||
return &api.DeleteConnectorResp{}, nil
|
||||
}
|
||||
|
||||
func (d dexAPI) ListConnectors(ctx context.Context, req *api.ListConnectorReq) (*api.ListConnectorResp, error) {
|
||||
if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
|
||||
return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
|
||||
}
|
||||
|
||||
connectorList, err := d.s.ListConnectors()
|
||||
if err != nil {
|
||||
d.logger.Error("api: failed to list connectors", "err", err)
|
||||
return nil, fmt.Errorf("list connectors: %v", err)
|
||||
}
|
||||
|
||||
connectors := make([]*api.Connector, 0, len(connectorList))
|
||||
for _, connector := range connectorList {
|
||||
c := api.Connector{
|
||||
Id: connector.ID,
|
||||
Name: connector.Name,
|
||||
Type: connector.Type,
|
||||
Config: connector.Config,
|
||||
}
|
||||
connectors = append(connectors, &c)
|
||||
}
|
||||
|
||||
return &api.ListConnectorResp{
|
||||
Connectors: connectors,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+237
-6
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -29,14 +30,14 @@ type apiClient struct {
|
||||
}
|
||||
|
||||
// newAPI constructs a gRCP client connected to a backing server.
|
||||
func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T) *apiClient {
|
||||
func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T, addtionalFeatures []AdditionalFeature) *apiClient {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serv := grpc.NewServer()
|
||||
api.RegisterDexServer(serv, NewAPI(s, logger, "test"))
|
||||
api.RegisterDexServer(serv, NewAPI(s, logger, "test", addtionalFeatures))
|
||||
go serv.Serve(l)
|
||||
|
||||
// NewClient will retry automatically if the serv.Serve() goroutine
|
||||
@@ -61,7 +62,7 @@ func TestPassword(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -170,7 +171,7 @@ func TestCheckCost(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{})
|
||||
defer client.Close()
|
||||
|
||||
tests := []struct {
|
||||
@@ -223,7 +224,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -332,7 +333,7 @@ func TestUpdateClient(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{})
|
||||
defer client.Close()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -490,3 +491,233 @@ func find(item string, items []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestCreateConnector(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
connectorID := "connector123"
|
||||
connectorName := "TestConnector"
|
||||
connectorType := "TestType"
|
||||
connectorConfig := []byte(`{"key": "value"}`)
|
||||
|
||||
createReq := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: connectorID,
|
||||
Name: connectorName,
|
||||
Type: connectorType,
|
||||
Config: connectorConfig,
|
||||
},
|
||||
}
|
||||
|
||||
// Test valid connector creation
|
||||
if resp, err := client.CreateConnector(ctx, &createReq); err != nil || resp.AlreadyExists {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create connector: %v", err)
|
||||
} else if resp.AlreadyExists {
|
||||
t.Fatalf("Unable to create connector since %s already exists", connectorID)
|
||||
}
|
||||
t.Fatalf("Unable to create connector: %v", err)
|
||||
}
|
||||
|
||||
// Test creating the same connector again (expecting failure)
|
||||
if resp, _ := client.CreateConnector(ctx, &createReq); !resp.AlreadyExists {
|
||||
t.Fatalf("Created connector %s twice", connectorID)
|
||||
}
|
||||
|
||||
createReq.Connector.Config = []byte("invalid_json")
|
||||
|
||||
// Test invalid JSON config
|
||||
if _, err := client.CreateConnector(ctx, &createReq); err == nil {
|
||||
t.Fatal("Expected an error for invalid JSON config, but none occurred")
|
||||
} else if !strings.Contains(err.Error(), "invalid config supplied") {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnector(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
connectorID := "connector123"
|
||||
newConnectorName := "UpdatedConnector"
|
||||
newConnectorType := "UpdatedType"
|
||||
newConnectorConfig := []byte(`{"updated_key": "updated_value"}`)
|
||||
|
||||
// Create a connector for testing
|
||||
createReq := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: connectorID,
|
||||
Name: "TestConnector",
|
||||
Type: "TestType",
|
||||
Config: []byte(`{"key": "value"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq)
|
||||
|
||||
updateReq := api.UpdateConnectorReq{
|
||||
Id: connectorID,
|
||||
NewName: newConnectorName,
|
||||
NewType: newConnectorType,
|
||||
NewConfig: newConnectorConfig,
|
||||
}
|
||||
|
||||
// Test valid connector update
|
||||
if _, err := client.UpdateConnector(ctx, &updateReq); err != nil {
|
||||
t.Fatalf("Unable to update connector: %v", err)
|
||||
}
|
||||
|
||||
resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for _, connector := range resp.Connectors {
|
||||
if connector.Id == connectorID {
|
||||
if connector.Name != newConnectorName {
|
||||
t.Fatal("connector name should have been updated")
|
||||
}
|
||||
if string(connector.Config) != string(newConnectorConfig) {
|
||||
t.Fatal("connector config should have been updated")
|
||||
}
|
||||
if connector.Type != newConnectorType {
|
||||
t.Fatal("connector type should have been updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateReq.NewConfig = []byte("invalid_json")
|
||||
|
||||
// Test invalid JSON config in update request
|
||||
if _, err := client.UpdateConnector(ctx, &updateReq); err == nil {
|
||||
t.Fatal("Expected an error for invalid JSON config in update, but none occurred")
|
||||
} else if !strings.Contains(err.Error(), "invalid config supplied") {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConnector(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
connectorID := "connector123"
|
||||
|
||||
// Create a connector for testing
|
||||
createReq := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: connectorID,
|
||||
Name: "TestConnector",
|
||||
Type: "TestType",
|
||||
Config: []byte(`{"key": "value"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq)
|
||||
|
||||
deleteReq := api.DeleteConnectorReq{
|
||||
Id: connectorID,
|
||||
}
|
||||
|
||||
// Test valid connector deletion
|
||||
if _, err := client.DeleteConnector(ctx, &deleteReq); err != nil {
|
||||
t.Fatalf("Unable to delete connector: %v", err)
|
||||
}
|
||||
|
||||
// Test non existent connector deletion
|
||||
resp, err := client.DeleteConnector(ctx, &deleteReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete connector: %v", err)
|
||||
}
|
||||
|
||||
if !resp.NotFound {
|
||||
t.Fatal("Should return not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListConnectors(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create connectors for testing
|
||||
createReq1 := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: "connector1",
|
||||
Name: "Connector1",
|
||||
Type: "Type1",
|
||||
Config: []byte(`{"key": "value1"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq1)
|
||||
|
||||
createReq2 := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: "connector2",
|
||||
Name: "Connector2",
|
||||
Type: "Type2",
|
||||
Config: []byte(`{"key": "value2"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq2)
|
||||
|
||||
listReq := api.ListConnectorReq{}
|
||||
|
||||
// Test listing connectors
|
||||
if resp, err := client.ListConnectors(ctx, &listReq); err != nil {
|
||||
t.Fatalf("Unable to list connectors: %v", err)
|
||||
} else if len(resp.Connectors) != 2 { // Check the number of connectors in the response
|
||||
t.Fatalf("Expected 2 connectors, found %d", len(resp.Connectors))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingAdditionalFeature(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t, []AdditionalFeature{})
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create connectors for testing
|
||||
createReq1 := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: "connector1",
|
||||
Name: "Connector1",
|
||||
Type: "Type1",
|
||||
Config: []byte(`{"key": "value1"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq1)
|
||||
|
||||
createReq2 := api.CreateConnectorReq{
|
||||
Connector: &api.Connector{
|
||||
Id: "connector2",
|
||||
Name: "Connector2",
|
||||
Type: "Type2",
|
||||
Config: []byte(`{"key": "value2"}`),
|
||||
},
|
||||
}
|
||||
client.CreateConnector(ctx, &createReq2)
|
||||
|
||||
listReq := api.ListConnectorReq{}
|
||||
|
||||
if _, err := client.ListConnectors(ctx, &listReq); err == nil {
|
||||
t.Fatal("ListConnectors should have returned an error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ import (
|
||||
"github.com/dexidp/dex/web"
|
||||
)
|
||||
|
||||
// AdditionalFeature allows the extension of Dex server functionalities
|
||||
type AdditionalFeature string
|
||||
|
||||
// ConnectorsCRUD is an additional feature that allows CRUD operations on connectors
|
||||
var ConnectorsCRUD AdditionalFeature = "ConnectorsCRUD"
|
||||
|
||||
var ValidAdditionalFeatures []AdditionalFeature = []AdditionalFeature{
|
||||
ConnectorsCRUD,
|
||||
}
|
||||
|
||||
// LocalConnector is the local passwordDB connector which is an internal
|
||||
// connector maintained by the server.
|
||||
const LocalConnector = "local"
|
||||
|
||||
Reference in New Issue
Block a user