Просмотр исходного кода

feat(go): add idp sync test api;

Vishal Dalwadi 1 месяц назад
Родитель
Сommit
41591d1ef5
5 измененных файлов с 171 добавлено и 3 удалено
  1. 9 0
      models/structs.go
  2. 44 0
      pro/controllers/users.go
  3. 85 3
      pro/idp/azure/azure.go
  4. 32 0
      pro/idp/google/google.go
  5. 1 0
      pro/idp/idp.go

+ 9 - 0
models/structs.go

@@ -407,3 +407,12 @@ type IDPSyncStatus struct {
 	// and describes the error when the sync fails.
 	Description string `json:"description"`
 }
+
+type IDPSyncTestRequest struct {
+	AuthProvider      string `json:"auth_provider"`
+	ClientID          string `json:"client_id"`
+	ClientSecret      string `json:"client_secret"`
+	AzureTenantID     string `json:"azure_tenant_id"`
+	GoogleAdminEmail  string `json:"google_admin_email"`
+	GoogleSACredsJson string `json:"google_sa_creds_json"`
+}

+ 44 - 0
pro/controllers/users.go

@@ -5,6 +5,9 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/gravitl/netmaker/pro/idp"
+	"github.com/gravitl/netmaker/pro/idp/azure"
+	"github.com/gravitl/netmaker/pro/idp/google"
 	"net/http"
 	"net/url"
 	"strings"
@@ -64,6 +67,7 @@ func UserHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
 
 	r.HandleFunc("/api/idp/sync", logic.SecurityCheck(true, http.HandlerFunc(syncIDP))).Methods(http.MethodPost)
+	r.HandleFunc("/api/idp/sync/test", logic.SecurityCheck(true, http.HandlerFunc(testIDPSync))).Methods(http.MethodPost)
 	r.HandleFunc("/api/idp/sync/status", logic.SecurityCheck(true, http.HandlerFunc(getIDPSyncStatus))).Methods(http.MethodGet)
 	r.HandleFunc("/api/idp", logic.SecurityCheck(true, http.HandlerFunc(removeIDPIntegration))).Methods(http.MethodDelete)
 }
@@ -1619,6 +1623,46 @@ func syncIDP(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponse(w, r, "starting sync from idp")
 }
 
+// @Summary     Test IDP Sync Credentials.
+// @Router      /api/idp/sync/test [post]
+// @Tags        IDP
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+func testIDPSync(w http.ResponseWriter, r *http.Request) {
+	var req models.IDPSyncTestRequest
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		err = fmt.Errorf("failed to decode request body: %v", err)
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	var idpClient idp.Client
+	switch req.AuthProvider {
+	case "google":
+		idpClient, err = google.NewGoogleWorkspaceClient(req.GoogleAdminEmail, req.GoogleSACredsJson)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	case "azure-ad":
+		idpClient = azure.NewAzureEntraIDClient(req.ClientID, req.ClientSecret, req.AzureTenantID)
+	default:
+		err = fmt.Errorf("invalid auth provider: %s", req.AuthProvider)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	err = idpClient.Verify()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	logic.ReturnSuccessResponse(w, r, "idp sync test successful")
+}
+
 // @Summary     Gets idp sync status.
 // @Router      /api/idp/sync/status [get]
 // @Tags        IDP

+ 85 - 3
pro/idp/azure/azure.go

@@ -30,6 +30,68 @@ func NewAzureEntraIDClientFromSettings() *Client {
 	return NewAzureEntraIDClient(settings.ClientID, settings.ClientSecret, settings.AzureTenant)
 }
 
+func (a *Client) Verify() error {
+	accessToken, err := a.getAccessToken()
+	if err != nil {
+		return err
+	}
+
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled&$top=1", nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("Authorization", "Bearer "+accessToken)
+	req.Header.Add("Accept", "application/json")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var users getUsersResponse
+	err = json.NewDecoder(resp.Body).Decode(&users)
+	if err != nil {
+		return err
+	}
+
+	if users.Error.Code != "" {
+		return errors.New(users.Error.Message)
+	}
+
+	req, err = http.NewRequest("GET", "https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$expand=members($select=id)&$top=1", nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("Authorization", "Bearer "+accessToken)
+	req.Header.Add("Accept", "application/json")
+
+	resp, err = client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var groups getGroupsResponse
+	err = json.NewDecoder(resp.Body).Decode(&groups)
+	if err != nil {
+		return err
+	}
+
+	if groups.Error.Code != "" {
+		return errors.New(groups.Error.Message)
+	}
+
+	return nil
+}
+
 func (a *Client) GetUsers() ([]idp.User, error) {
 	accessToken, err := a.getAccessToken()
 	if err != nil {
@@ -59,6 +121,10 @@ func (a *Client) GetUsers() ([]idp.User, error) {
 		return nil, err
 	}
 
+	if users.Error.Code != "" {
+		return nil, errors.New(users.Error.Message)
+	}
+
 	retval := make([]idp.User, len(users.Value))
 	for i, user := range users.Value {
 		retval[i] = idp.User{
@@ -101,6 +167,10 @@ func (a *Client) GetGroups() ([]idp.Group, error) {
 		return nil, err
 	}
 
+	if groups.Error.Code != "" {
+		return nil, errors.New(groups.Error.Message)
+	}
+
 	retval := make([]idp.Group, len(groups.Value))
 	for i, group := range groups.Value {
 		retvalMembers := make([]string, len(group.Members))
@@ -145,11 +215,12 @@ func (a *Client) getAccessToken() (string, error) {
 		return token, nil
 	}
 
-	return "", errors.New("failed to get access token")
+	return "", errors.New("invalid credentials")
 }
 
 type getUsersResponse struct {
-	OdataContext string `json:"@odata.context"`
+	Error        errorResponse `json:"error"`
+	OdataContext string        `json:"@odata.context"`
 	Value        []struct {
 		Id                string `json:"id"`
 		UserPrincipalName string `json:"userPrincipalName"`
@@ -159,7 +230,8 @@ type getUsersResponse struct {
 }
 
 type getGroupsResponse struct {
-	OdataContext string `json:"@odata.context"`
+	Error        errorResponse `json:"error"`
+	OdataContext string        `json:"@odata.context"`
 	Value        []struct {
 		Id          string `json:"id"`
 		DisplayName string `json:"displayName"`
@@ -169,3 +241,13 @@ type getGroupsResponse struct {
 		} `json:"members"`
 	} `json:"value"`
 }
+
+type errorResponse struct {
+	Code       string `json:"code"`
+	Message    string `json:"message"`
+	InnerError struct {
+		Date            string `json:"date"`
+		RequestId       string `json:"request-id"`
+		ClientRequestId string `json:"client-request-id"`
+	} `json:"innerError"`
+}

+ 32 - 0
pro/idp/google/google.go

@@ -4,9 +4,11 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/pro/idp"
 	admindir "google.golang.org/api/admin/directory/v1"
+	"google.golang.org/api/googleapi"
 	"google.golang.org/api/impersonate"
 	"google.golang.org/api/option"
 )
@@ -63,6 +65,36 @@ func NewGoogleWorkspaceClientFromSettings() (*Client, error) {
 	return NewGoogleWorkspaceClient(settings.GoogleAdminEmail, settings.GoogleSACredsJson)
 }
 
+func (g *Client) Verify() error {
+	_, err := g.service.Users.List().
+		Customer("my_customer").
+		MaxResults(1).
+		Do()
+	if err != nil {
+		var gerr *googleapi.Error
+		if errors.As(err, &gerr) {
+			return errors.New(gerr.Message)
+		}
+
+		return err
+	}
+
+	_, err = g.service.Groups.List().
+		Customer("my_customer").
+		MaxResults(1).
+		Do()
+	if err != nil {
+		var gerr *googleapi.Error
+		if errors.As(err, &gerr) {
+			return errors.New(gerr.Message)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 func (g *Client) GetUsers() ([]idp.User, error) {
 	var retval []idp.User
 	err := g.service.Users.List().

+ 1 - 0
pro/idp/idp.go

@@ -1,6 +1,7 @@
 package idp
 
 type Client interface {
+	Verify() error
 	GetUsers() ([]User, error)
 	GetGroups() ([]Group, error)
 }