mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
Merge pull request #4202 from daemonfire300/jf-grpc-list-clients-3496
feat: grpc api list clients
This commit is contained in:
+615
-422
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-1
@@ -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 {
|
||||
|
||||
@@ -733,3 +733,100 @@ func TestMissingConnectorsCRUDFeatureFlag(t *testing.T) {
|
||||
t.Fatal("ListConnectors should have returned an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
|
||||
s := memory.New(logger)
|
||||
client := newAPI(s, logger, t)
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// List Clients
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user