mirror of
https://github.com/netbirdio/netbird.git
synced 2025-06-03 08:35:43 +02:00
Implement user deletion across all IDP-ss. Expires all user peers when the user is deleted. Users are permanently removed from a local store, but in IDP, we remove Netbird attributes for the user untilUserDeleteFromIDPEnabled setting is not enabled. To test, an admin user should remove any additional users. Until the UI incorporates this feature, use a curl DELETE request targeting the /users/<USER_ID> management endpoint. Note that this request only removes user attributes and doesn't trigger a delete from the IDP. To enable user removal from the IdP, set UserDeleteFromIDPEnabled to true in account settings. Until we have a UI for this, make this change directly in the store file. Store the deleted email addresses in encrypted in activity store.
366 lines
9.6 KiB
Go
366 lines
9.6 KiB
Go
package idp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/oauth2/google"
|
|
admin "google.golang.org/api/admin/directory/v1"
|
|
"google.golang.org/api/googleapi"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
// GoogleWorkspaceManager Google Workspace manager client instance.
|
|
type GoogleWorkspaceManager struct {
|
|
usersService *admin.UsersService
|
|
CustomerID string
|
|
httpClient ManagerHTTPClient
|
|
credentials ManagerCredentials
|
|
helper ManagerHelper
|
|
appMetrics telemetry.AppMetrics
|
|
}
|
|
|
|
// GoogleWorkspaceClientConfig Google Workspace manager client configurations.
|
|
type GoogleWorkspaceClientConfig struct {
|
|
ServiceAccountKey string
|
|
CustomerID string
|
|
}
|
|
|
|
// GoogleWorkspaceCredentials Google Workspace authentication information.
|
|
type GoogleWorkspaceCredentials struct {
|
|
clientConfig GoogleWorkspaceClientConfig
|
|
helper ManagerHelper
|
|
httpClient ManagerHTTPClient
|
|
appMetrics telemetry.AppMetrics
|
|
}
|
|
|
|
func (gc *GoogleWorkspaceCredentials) Authenticate() (JWTToken, error) {
|
|
return JWTToken{}, nil
|
|
}
|
|
|
|
// NewGoogleWorkspaceManager creates a new instance of the GoogleWorkspaceManager.
|
|
func NewGoogleWorkspaceManager(config GoogleWorkspaceClientConfig, appMetrics telemetry.AppMetrics) (*GoogleWorkspaceManager, error) {
|
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
httpTransport.MaxIdleConns = 5
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: httpTransport,
|
|
}
|
|
helper := JsonParser{}
|
|
|
|
if config.CustomerID == "" {
|
|
return nil, fmt.Errorf("google IdP configuration is incomplete, CustomerID is missing")
|
|
}
|
|
|
|
credentials := &GoogleWorkspaceCredentials{
|
|
clientConfig: config,
|
|
httpClient: httpClient,
|
|
helper: helper,
|
|
appMetrics: appMetrics,
|
|
}
|
|
|
|
// Create a new Admin SDK Directory service client
|
|
adminCredentials, err := getGoogleCredentials(config.ServiceAccountKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
service, err := admin.NewService(context.Background(),
|
|
option.WithScopes(admin.AdminDirectoryUserScope, admin.AdminDirectoryUserschemaScope),
|
|
option.WithCredentials(adminCredentials),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = configureAppMetadataSchema(service, config.CustomerID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &GoogleWorkspaceManager{
|
|
usersService: service.Users,
|
|
CustomerID: config.CustomerID,
|
|
httpClient: httpClient,
|
|
credentials: credentials,
|
|
helper: helper,
|
|
appMetrics: appMetrics,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
|
func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
|
metadata, err := gm.helper.Marshal(appMetadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user := &admin.User{
|
|
CustomSchemas: map[string]googleapi.RawMessage{
|
|
"app_metadata": metadata,
|
|
},
|
|
}
|
|
|
|
_, err = gm.usersService.Update(userID, user).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserDataByID requests user data from Google Workspace via ID.
|
|
func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
|
user, err := gm.usersService.Get(userID).Projection("full").Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
|
}
|
|
|
|
return parseGoogleWorkspaceUser(user)
|
|
}
|
|
|
|
// GetAccount returns all the users for a given profile.
|
|
func (gm *GoogleWorkspaceManager) GetAccount(accountID string) ([]*UserData, error) {
|
|
query := fmt.Sprintf("app_metadata.wt_account_id=\"%s\"", accountID)
|
|
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Query(query).Projection("full").Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
usersData := make([]*UserData, 0)
|
|
for _, user := range usersList.Users {
|
|
userData, err := parseGoogleWorkspaceUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
usersData = append(usersData, userData)
|
|
}
|
|
|
|
return usersData, nil
|
|
}
|
|
|
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
|
// It returns a list of users indexed by accountID.
|
|
func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Projection("full").Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
|
}
|
|
|
|
indexedUsers := make(map[string][]*UserData)
|
|
for _, user := range usersList.Users {
|
|
userData, err := parseGoogleWorkspaceUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
accountID := userData.AppMetadata.WTAccountID
|
|
if accountID != "" {
|
|
if _, ok := indexedUsers[accountID]; !ok {
|
|
indexedUsers[accountID] = make([]*UserData, 0)
|
|
}
|
|
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
}
|
|
}
|
|
|
|
return indexedUsers, nil
|
|
}
|
|
|
|
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
|
func (gm *GoogleWorkspaceManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
|
invite := true
|
|
metadata := AppMetadata{
|
|
WTAccountID: accountID,
|
|
WTPendingInvite: &invite,
|
|
}
|
|
|
|
username := &admin.UserName{}
|
|
fields := strings.Fields(name)
|
|
if n := len(fields); n > 0 {
|
|
username.GivenName = strings.Join(fields[:n-1], " ")
|
|
username.FamilyName = fields[n-1]
|
|
}
|
|
|
|
payload, err := gm.helper.Marshal(metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user := &admin.User{
|
|
Name: username,
|
|
PrimaryEmail: email,
|
|
CustomSchemas: map[string]googleapi.RawMessage{
|
|
"app_metadata": payload,
|
|
},
|
|
Password: GeneratePassword(8, 1, 1, 1),
|
|
}
|
|
user, err = gm.usersService.Insert(user).Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountCreateUser()
|
|
}
|
|
|
|
return parseGoogleWorkspaceUser(user)
|
|
}
|
|
|
|
// GetUserByEmail searches users with a given email.
|
|
// If no users have been found, this function returns an empty list.
|
|
func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|
user, err := gm.usersService.Get(email).Projection("full").Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
|
}
|
|
|
|
userData, err := parseGoogleWorkspaceUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
users := make([]*UserData, 0)
|
|
users = append(users, userData)
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// InviteUserByID resend invitations to users who haven't activated,
|
|
// their accounts prior to the expiration period.
|
|
func (gm *GoogleWorkspaceManager) InviteUserByID(_ string) error {
|
|
return fmt.Errorf("method InviteUserByID not implemented")
|
|
}
|
|
|
|
// DeleteUser from GoogleWorkspace.
|
|
func (gm *GoogleWorkspaceManager) DeleteUser(userID string) error {
|
|
if err := gm.usersService.Delete(userID).Do(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if gm.appMetrics != nil {
|
|
gm.appMetrics.IDPMetrics().CountDeleteUser()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getGoogleCredentials retrieves Google credentials based on the provided serviceAccountKey.
|
|
// It decodes the base64-encoded serviceAccountKey and attempts to obtain credentials using it.
|
|
// If that fails, it falls back to using the default Google credentials path.
|
|
// It returns the retrieved credentials or an error if unsuccessful.
|
|
func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error) {
|
|
log.Debug("retrieving google credentials from the base64 encoded service account key")
|
|
decodeKey, err := base64.StdEncoding.DecodeString(serviceAccountKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode service account key: %w", err)
|
|
}
|
|
|
|
creds, err := google.CredentialsFromJSON(
|
|
context.Background(),
|
|
decodeKey,
|
|
admin.AdminDirectoryUserschemaScope,
|
|
admin.AdminDirectoryUserScope,
|
|
)
|
|
if err == nil {
|
|
// No need to fallback to the default Google credentials path
|
|
return creds, nil
|
|
}
|
|
|
|
log.Debugf("failed to retrieve Google credentials from ServiceAccountKey: %v", err)
|
|
log.Debug("falling back to default google credentials location")
|
|
|
|
creds, err = google.FindDefaultCredentials(
|
|
context.Background(),
|
|
admin.AdminDirectoryUserschemaScope,
|
|
admin.AdminDirectoryUserScope,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return creds, nil
|
|
}
|
|
|
|
// configureAppMetadataSchema create a custom schema for managing app metadata fields in Google Workspace.
|
|
func configureAppMetadataSchema(service *admin.Service, customerID string) error {
|
|
schemaList, err := service.Schemas.List(customerID).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// checks if app_metadata schema is already created
|
|
for _, schema := range schemaList.Schemas {
|
|
if schema.SchemaName == "app_metadata" {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// create new app_metadata schema
|
|
appMetadataSchema := &admin.Schema{
|
|
SchemaName: "app_metadata",
|
|
Fields: []*admin.SchemaFieldSpec{
|
|
{
|
|
FieldName: "wt_account_id",
|
|
FieldType: "STRING",
|
|
MultiValued: false,
|
|
},
|
|
{
|
|
FieldName: "wt_pending_invite",
|
|
FieldType: "BOOL",
|
|
MultiValued: false,
|
|
},
|
|
},
|
|
}
|
|
_, err = service.Schemas.Insert(customerID, appMetadataSchema).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseGoogleWorkspaceUser parse google user to UserData.
|
|
func parseGoogleWorkspaceUser(user *admin.User) (*UserData, error) {
|
|
var appMetadata AppMetadata
|
|
|
|
// Get app metadata from custom schemas
|
|
if user.CustomSchemas != nil {
|
|
rawMessage := user.CustomSchemas["app_metadata"]
|
|
helper := JsonParser{}
|
|
|
|
if err := helper.Unmarshal(rawMessage, &appMetadata); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &UserData{
|
|
ID: user.Id,
|
|
Email: user.PrimaryEmail,
|
|
Name: user.Name.FullName,
|
|
AppMetadata: appMetadata,
|
|
}, nil
|
|
}
|