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;
}
// 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.
message GetClientReq {
// The ID of the client.
@ -63,6 +73,14 @@ message UpdateClientResp {
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.
// Password is an email for password mapping managed by the storage.
@ -253,6 +271,8 @@ service Dex {
rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {};
// DeleteClient deletes the provided client.
rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {};
// ListClients lists all client entries.
rpc ListClients(ListClientReq) returns (ListClientResp) {};
// CreatePassword creates a password.
rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {};
// UpdatePassword modifies existing password.

40
api/v2/api_grpc.pb.go

@ -23,6 +23,7 @@ const (
Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient"
Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient"
Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient"
Dex_ListClients_FullMethodName = "/api.Dex/ListClients"
Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword"
Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword"
Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword"
@ -52,6 +53,8 @@ type DexClient interface {
UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
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(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
@ -130,6 +133,16 @@ func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts
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) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreatePasswordResp)
@ -274,6 +287,8 @@ type DexServer interface {
UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error)
// ListClients lists all client entries.
ListClients(context.Context, *ListClientReq) (*ListClientResp, error)
// CreatePassword creates a password.
CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
@ -324,6 +339,9 @@ func (UnimplementedDexServer) UpdateClient(context.Context, *UpdateClientReq) (*
func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) {
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) {
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)
}
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) {
in := new(CreatePasswordReq)
if err := dec(in); err != nil {
@ -713,6 +749,10 @@ var Dex_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteClient",
Handler: _Dex_DeleteClient_Handler,
},
{
MethodName: "ListClients",
Handler: _Dex_ListClients_Handler,
},
{
MethodName: "CreatePassword",
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
3. VerifyPassword
4. DeletePassword
5. CreateClient
6. ListClients
7. DeleteClient
## Cleaning up

57
examples/grpc-client/client.go

@ -58,7 +58,7 @@ func createPassword(cli api.DexClient) error {
// Create password.
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("failed to create password: %v", err)
@ -125,6 +125,57 @@ func createPassword(cli api.DexClient) error {
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() {
caCrt := flag.String("ca-crt", "", "CA certificate")
clientCrt := flag.String("client-crt", "", "Client certificate")
@ -143,4 +194,8 @@ func main() {
if err := createPassword(client); err != nil {
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
// to determine if the server supports specific features.
const apiVersion = 2
const apiVersion = 3
const (
// 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
}
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
// bound cost requirements.
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")
}
}
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