Browse Source

feat: grpc api list clients

refers to https://github.com/dexidp/dex/issues/3496

Signed-off-by: Julius Foitzik <info@accountr.eu>
pull/4202/head
Julius Foitzik 9 months ago
parent
commit
e7b151c386
No known key found for this signature in database
GPG Key ID: DF557DB5A1CE5C8B
  1. 1037
      api/v2/api.pb.go
  2. 20
      api/v2/api.proto
  3. 40
      api/v2/api_grpc.pb.go
  4. 3
      examples/grpc-client/README.md
  5. 57
      examples/grpc-client/client.go
  6. 27
      server/api.go
  7. 96
      server/api_test.go

1037
api/v2/api.pb.go

File diff suppressed because it is too large Load Diff

20
api/v2/api.proto

@ -16,6 +16,16 @@ message Client {
string logo_url = 7; string logo_url = 7;
} }
// ClientInfo represents an OAuth2 client without sensitive information.
message ClientInfo {
string id = 1;
repeated string redirect_uris = 2;
repeated string trusted_peers = 3;
bool public = 4;
string name = 5;
string logo_url = 6;
}
// GetClientReq is a request to retrieve client details. // GetClientReq is a request to retrieve client details.
message GetClientReq { message GetClientReq {
// The ID of the client. // The ID of the client.
@ -63,6 +73,14 @@ message UpdateClientResp {
bool not_found = 1; bool not_found = 1;
} }
// ListClientReq is a request to enumerate clients.
message ListClientReq {}
// ListClientResp returns a list of clients.
message ListClientResp {
repeated ClientInfo clients = 1;
}
// TODO(ericchiang): expand this. // TODO(ericchiang): expand this.
// Password is an email for password mapping managed by the storage. // Password is an email for password mapping managed by the storage.
@ -253,6 +271,8 @@ service Dex {
rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {}; rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {};
// DeleteClient deletes the provided client. // DeleteClient deletes the provided client.
rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {}; rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {};
// ListClients lists all client entries.
rpc ListClients(ListClientReq) returns (ListClientResp) {};
// CreatePassword creates a password. // CreatePassword creates a password.
rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {}; rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {};
// UpdatePassword modifies existing password. // UpdatePassword modifies existing password.

40
api/v2/api_grpc.pb.go

@ -23,6 +23,7 @@ const (
Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient" Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient"
Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient" Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient"
Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient" Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient"
Dex_ListClients_FullMethodName = "/api.Dex/ListClients"
Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword" Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword"
Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword" Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword"
Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword" Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword"
@ -52,6 +53,8 @@ type DexClient interface {
UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error)
// DeleteClient deletes the provided client. // DeleteClient deletes the provided client.
DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error)
// ListClients lists all client entries.
ListClients(ctx context.Context, in *ListClientReq, opts ...grpc.CallOption) (*ListClientResp, error)
// CreatePassword creates a password. // CreatePassword creates a password.
CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password. // UpdatePassword modifies existing password.
@ -130,6 +133,16 @@ func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts
return out, nil return out, nil
} }
func (c *dexClient) ListClients(ctx context.Context, in *ListClientReq, opts ...grpc.CallOption) (*ListClientResp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListClientResp)
err := c.cc.Invoke(ctx, Dex_ListClients_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) { func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreatePasswordResp) out := new(CreatePasswordResp)
@ -274,6 +287,8 @@ type DexServer interface {
UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error) UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error)
// DeleteClient deletes the provided client. // DeleteClient deletes the provided client.
DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error)
// ListClients lists all client entries.
ListClients(context.Context, *ListClientReq) (*ListClientResp, error)
// CreatePassword creates a password. // CreatePassword creates a password.
CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password. // UpdatePassword modifies existing password.
@ -324,6 +339,9 @@ func (UnimplementedDexServer) UpdateClient(context.Context, *UpdateClientReq) (*
func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) { func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteClient not implemented") return nil, status.Errorf(codes.Unimplemented, "method DeleteClient not implemented")
} }
func (UnimplementedDexServer) ListClients(context.Context, *ListClientReq) (*ListClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListClients not implemented")
}
func (UnimplementedDexServer) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) { func (UnimplementedDexServer) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePassword not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreatePassword not implemented")
} }
@ -456,6 +474,24 @@ func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(in
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Dex_ListClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).ListClients(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Dex_ListClients_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).ListClients(ctx, req.(*ListClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePasswordReq) in := new(CreatePasswordReq)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -713,6 +749,10 @@ var Dex_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteClient", MethodName: "DeleteClient",
Handler: _Dex_DeleteClient_Handler, Handler: _Dex_DeleteClient_Handler,
}, },
{
MethodName: "ListClients",
Handler: _Dex_ListClients_Handler,
},
{ {
MethodName: "CreatePassword", MethodName: "CreatePassword",
Handler: _Dex_CreatePassword_Handler, Handler: _Dex_CreatePassword_Handler,

3
examples/grpc-client/README.md

@ -50,6 +50,9 @@ Running the gRPC client will cause the following API calls to be made to the ser
2. ListPasswords 2. ListPasswords
3. VerifyPassword 3. VerifyPassword
4. DeletePassword 4. DeletePassword
5. CreateClient
6. ListClients
7. DeleteClient
## Cleaning up ## Cleaning up

57
examples/grpc-client/client.go

@ -58,7 +58,7 @@ func createPassword(cli api.DexClient) error {
// Create password. // Create password.
if resp, err := cli.CreatePassword(context.TODO(), createReq); err != nil || resp.AlreadyExists { if resp, err := cli.CreatePassword(context.TODO(), createReq); err != nil || resp.AlreadyExists {
if resp != nil && resp.AlreadyExists { if resp != nil && resp.AlreadyExists {
return fmt.Errorf("Password %s already exists", createReq.Password.Email) return fmt.Errorf("Password %s already exists", createReq.Password.Email)
} }
return fmt.Errorf("failed to create password: %v", err) return fmt.Errorf("failed to create password: %v", err)
@ -125,6 +125,57 @@ func createPassword(cli api.DexClient) error {
return nil return nil
} }
func createAndListClients(cli api.DexClient) error {
client := &api.Client{
Id: "example-client",
Secret: "example-secret",
RedirectUris: []string{"http://localhost:8080/callback"},
TrustedPeers: []string{},
Public: false,
Name: "Example Client",
LogoUrl: "http://example.com/logo.png",
}
createReq := &api.CreateClientReq{
Client: client,
}
if resp, err := cli.CreateClient(context.TODO(), createReq); err != nil || resp.AlreadyExists {
if resp != nil && resp.AlreadyExists {
log.Printf("Client %s already exists", createReq.Client.Id)
} else {
return fmt.Errorf("failed to create client: %v", err)
}
} else {
log.Printf("Created client with ID %s", createReq.Client.Id)
}
listResp, err := cli.ListClients(context.TODO(), &api.ListClientReq{})
if err != nil {
return fmt.Errorf("failed to list clients: %v", err)
}
log.Print("Listing Clients:\n")
for _, client := range listResp.Clients {
log.Printf("ID: %s, Name: %s, Public: %t, RedirectURIs: %v",
client.Id, client.Name, client.Public, client.RedirectUris)
}
deleteReq := &api.DeleteClientReq{
Id: client.Id,
}
if resp, err := cli.DeleteClient(context.TODO(), deleteReq); err != nil || resp.NotFound {
if resp != nil && resp.NotFound {
return fmt.Errorf("Client %s not found", deleteReq.Id)
}
return fmt.Errorf("failed to delete client: %v", err)
}
log.Printf("Deleted client with ID %s", deleteReq.Id)
return nil
}
func main() { func main() {
caCrt := flag.String("ca-crt", "", "CA certificate") caCrt := flag.String("ca-crt", "", "CA certificate")
clientCrt := flag.String("client-crt", "", "Client certificate") clientCrt := flag.String("client-crt", "", "Client certificate")
@ -143,4 +194,8 @@ func main() {
if err := createPassword(client); err != nil { if err := createPassword(client); err != nil {
log.Fatalf("testPassword failed: %v", err) log.Fatalf("testPassword failed: %v", err)
} }
if err := createAndListClients(client); err != nil {
log.Fatalf("testClients failed: %v", err)
}
} }

27
server/api.go

@ -18,7 +18,7 @@ import (
// apiVersion increases every time a new call is added to the API. Clients should use this info // apiVersion increases every time a new call is added to the API. Clients should use this info
// to determine if the server supports specific features. // to determine if the server supports specific features.
const apiVersion = 2 const apiVersion = 3
const ( const (
// recCost is the recommended bcrypt cost, which balances hash strength and // recCost is the recommended bcrypt cost, which balances hash strength and
@ -145,6 +145,31 @@ func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*ap
return &api.DeleteClientResp{}, nil return &api.DeleteClientResp{}, nil
} }
func (d dexAPI) ListClients(ctx context.Context, req *api.ListClientReq) (*api.ListClientResp, error) {
clientList, err := d.s.ListClients(ctx)
if err != nil {
d.logger.Error("failed to list clients", "err", err)
return nil, fmt.Errorf("list clients: %v", err)
}
clients := make([]*api.ClientInfo, 0, len(clientList))
for _, client := range clientList {
c := api.ClientInfo{
Id: client.ID,
Name: client.Name,
RedirectUris: client.RedirectURIs,
TrustedPeers: client.TrustedPeers,
Public: client.Public,
LogoUrl: client.LogoURL,
}
clients = append(clients, &c)
}
return &api.ListClientResp{
Clients: clients,
}, nil
}
// checkCost returns an error if the hash provided does not meet lower or upper // checkCost returns an error if the hash provided does not meet lower or upper
// bound cost requirements. // bound cost requirements.
func checkCost(hash []byte) error { func checkCost(hash []byte) error {

96
server/api_test.go

@ -733,3 +733,99 @@ func TestMissingConnectorsCRUDFeatureFlag(t *testing.T) {
t.Fatal("ListConnectors should have returned an error") t.Fatal("ListConnectors should have returned an error")
} }
} }
func TestListClients(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
s := memory.New(logger)
client := newAPI(s, logger, t)
defer client.Close()
ctx := context.Background()
listResp, err := client.ListClients(ctx, &api.ListClientReq{})
if err != nil {
t.Fatalf("Unable to list clients: %v", err)
}
if len(listResp.Clients) != 0 {
t.Fatalf("Expected 0 clients, got %d", len(listResp.Clients))
}
client1 := &api.Client{
Id: "client1",
Secret: "secret1",
RedirectUris: []string{"http://localhost:8080/callback"},
TrustedPeers: []string{"peer1"},
Public: false,
Name: "Test Client 1",
LogoUrl: "http://example.com/logo1.png",
}
client2 := &api.Client{
Id: "client2",
Secret: "secret2",
RedirectUris: []string{"http://localhost:8081/callback"},
TrustedPeers: []string{"peer2"},
Public: true,
Name: "Test Client 2",
LogoUrl: "http://example.com/logo2.png",
}
_, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client1})
if err != nil {
t.Fatalf("Unable to create client1: %v", err)
}
_, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client2})
if err != nil {
t.Fatalf("Unable to create client2: %v", err)
}
listResp, err = client.ListClients(ctx, &api.ListClientReq{})
if err != nil {
t.Fatalf("Unable to list clients: %v", err)
}
if len(listResp.Clients) != 2 {
t.Fatalf("Expected 2 clients, got %d", len(listResp.Clients))
}
clientMap := make(map[string]*api.ClientInfo)
for _, c := range listResp.Clients {
clientMap[c.Id] = c
}
if c1, exists := clientMap["client1"]; !exists {
t.Fatal("client1 not found in list")
} else {
if c1.Name != "Test Client 1" {
t.Errorf("Expected client1 name 'Test Client 1', got '%s'", c1.Name)
}
if len(c1.RedirectUris) != 1 || c1.RedirectUris[0] != "http://localhost:8080/callback" {
t.Errorf("Expected client1 redirect URIs ['http://localhost:8080/callback'], got %v", c1.RedirectUris)
}
if c1.Public != false {
t.Errorf("Expected client1 public false, got %v", c1.Public)
}
if c1.LogoUrl != "http://example.com/logo1.png" {
t.Errorf("Expected client1 logo URL 'http://example.com/logo1.png', got '%s'", c1.LogoUrl)
}
}
if c2, exists := clientMap["client2"]; !exists {
t.Fatal("client2 not found in list")
} else {
if c2.Name != "Test Client 2" {
t.Errorf("Expected client2 name 'Test Client 2', got '%s'", c2.Name)
}
if len(c2.RedirectUris) != 1 || c2.RedirectUris[0] != "http://localhost:8081/callback" {
t.Errorf("Expected client2 redirect URIs ['http://localhost:8081/callback'], got %v", c2.RedirectUris)
}
if c2.Public != true {
t.Errorf("Expected client2 public true, got %v", c2.Public)
}
if c2.LogoUrl != "http://example.com/logo2.png" {
t.Errorf("Expected client2 logo URL 'http://example.com/logo2.png', got '%s'", c2.LogoUrl)
}
}
}

Loading…
Cancel
Save