diff --git a/drivers/backup/auth.go b/drivers/backup/auth.go index e930cca1f..3008fc532 100644 --- a/drivers/backup/auth.go +++ b/drivers/backup/auth.go @@ -962,6 +962,41 @@ func GetMembersOfGroup(group string) ([]string, error) { return members, nil } +// GetMembersOfGroupWithID fetches all available members of the group with groupID +func GetMembersOfGroupWithID(groupID string) ([]string, error) { + fn := "GetMembersOfGroup" + keycloakEndPoint, err := getKeycloakEndPoint(true) + if err != nil { + return nil, err + } + + reqURL := fmt.Sprintf("%s/groups/%s/members", keycloakEndPoint, groupID) + method := "GET" + headers, err := GetCommonHTTPHeaders(PxCentralAdminUser, PxCentralAdminPwd) + if err != nil { + log.Errorf("%s: %v", fn, err) + return nil, err + } + + response, err := processHTTPRequest(method, reqURL, headers, nil) + if err != nil { + log.Errorf("%s: %v", fn, err) + return nil, err + } + var members []string + var users []KeycloakGroupMemberRepresentation + err = json.Unmarshal(response, &users) + if err != nil { + log.Errorf("%s: %v", fn, err) + return nil, err + } + for _, userName := range users { + members = append(members, userName.Name) + } + log.Debugf("list of members : %v", members) + return members, nil +} + // AddGroup adds a new group func AddGroup(group string) error { fn := "AddGroup" diff --git a/drivers/backup/backup.go b/drivers/backup/backup.go index f4afef89d..11c580de5 100644 --- a/drivers/backup/backup.go +++ b/drivers/backup/backup.go @@ -191,6 +191,12 @@ type Cluster interface { // GetClusterStatus returns status of the given cluster name in an organization GetClusterStatus(orgID string, clusterName string, ctx context.Context) (api.ClusterInfo_StatusInfo_Status, error) + + // ShareCluster share the cluster object + ShareCluster(ctx context.Context, req *api.ShareClusterRequest) (*api.ShareClusterResponse, error) + + // UnShareCluster unshare the cluster object + UnShareCluster(ctx context.Context, req *api.UnShareClusterRequest) (*api.UnShareClusterResponse, error) } // BLocation obj interface diff --git a/drivers/backup/portworx/portworx.go b/drivers/backup/portworx/portworx.go index b338471e7..c33208d4c 100644 --- a/drivers/backup/portworx/portworx.go +++ b/drivers/backup/portworx/portworx.go @@ -542,6 +542,14 @@ func (p *portworx) GetClusterStatus(orgID string, clusterName string, ctx contex return api.ClusterInfo_StatusInfo_Invalid, fmt.Errorf("cluster with name '%s' not found for org '%s'", clusterName, orgID) } +func (p *portworx) ShareCluster(ctx context.Context, req *api.ShareClusterRequest) (*api.ShareClusterResponse, error) { + return p.clusterManager.ShareCluster(ctx, req) +} + +func (p *portworx) UnShareCluster(ctx context.Context, req *api.UnShareClusterRequest) (*api.UnShareClusterResponse, error) { + return p.clusterManager.UnShareCluster(ctx, req) +} + // SetMissingClusterUID sets the missing cluster UID for cluster-related requests func (p *portworx) SetMissingClusterUID(ctx context.Context, req interface{}) (interface{}, error) { switch r := req.(type) { diff --git a/tests/backup_helper.go b/tests/backup_helper.go index fe2531a26..edf3de6dc 100644 --- a/tests/backup_helper.go +++ b/tests/backup_helper.go @@ -2457,6 +2457,28 @@ func GetAllBackupsAdmin() ([]string, error) { return backupNames, nil } +// GetAllBackupsFromCluster returns all the backups from a cluster +func GetAllBackupsFromCluster(ctx context1.Context, clusterName string, clusterUid string) ([]string, error) { + backupNames := make([]string, 0) + backupDriver := Inst().Backup + + backupEnumerateReq := &api.BackupEnumerateRequest{ + OrgId: BackupOrgID, + EnumerateOptions: &api.EnumerateOptions{ + ClusterNameFilter: clusterName, + ClusterUidFilter: clusterUid, + }, + } + currentBackups, err := backupDriver.EnumerateBackup(ctx, backupEnumerateReq) + if err != nil { + return nil, err + } + for _, backup := range currentBackups.GetBackups() { + backupNames = append(backupNames, backup.GetName()) + } + return backupNames, nil +} + // GetAllRestoresAdmin returns all the backups that px-central-admin has access to func GetAllRestoresAdmin() ([]string, error) { restoreNames := make([]string, 0) @@ -10471,3 +10493,341 @@ func IsPxInstalled() bool { numOfNodes := len(node.GetStorageDriverNodes()) return numOfNodes > 0 } + +// ShareCluster shares a cluster with users and groups and optionally shares existing backups. +func ShareCluster(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string, shareClusterBackups bool) (*api.ShareClusterResponse, error) { + backupDriver := Inst().Backup + shareClusterRequest := &api.ShareClusterRequest{ + OrgId: BackupOrgID, + ClusterRef: &api.ObjectRef{ + Name: clusterName, + Uid: clusterUid, + }, + // userid of the user(s) to share the cluster with + Users: users, + // group(s) to share the cluster with + Groups: groups, + // share_cluster_backups share is optional, if set to true, it will additionally share existing backups + ShareClusterBackups: shareClusterBackups, + } + resp, err := backupDriver.ShareCluster(ctx, shareClusterRequest) + if err != nil { + return resp, err + } + return resp, nil +} + +// ShareClusterWithValidation shares a cluster with users and groups and optionally shares existing backups and validates the share. +func ShareClusterWithValidation(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string, shareClusterBackups bool) (*api.ShareClusterResponse, error) { + backupDriver := Inst().Backup + shareClusterRequest := &api.ShareClusterRequest{ + OrgId: BackupOrgID, + ClusterRef: &api.ObjectRef{ + Name: clusterName, + Uid: clusterUid, + }, + // userid of the user(s) to share the cluster with + Users: users, + // group(s) to share the cluster with + Groups: groups, + // share_cluster_backups share is optional, if set to true, it will additionally share existing backups + ShareClusterBackups: shareClusterBackups, + } + resp, err := backupDriver.ShareCluster(ctx, shareClusterRequest) + if err != nil { + return resp, err + } + // Validate the share cluster + err = ValidateShareCluster(ctx, clusterName, clusterUid, users, groups) + if err != nil { + return nil, err + } + + if shareClusterBackups { + err = ValidateShareClusterBackup(ctx, clusterName, clusterUid, users, groups) + if err != nil { + return nil, err + } + } + return resp, nil +} + +// UnShareCluster unshares a cluster with users and groups. +func UnShareCluster(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string) (*api.UnShareClusterResponse, error) { + backupDriver := Inst().Backup + unshareClusterRequest := &api.UnShareClusterRequest{ + OrgId: BackupOrgID, + ClusterRef: &api.ObjectRef{ + Name: clusterName, + Uid: clusterUid, + }, + // userid of the user(s) to share the cluster with + Users: users, + // group(s) to share the cluster with + Groups: groups, + } + resp, err := backupDriver.UnShareCluster(ctx, unshareClusterRequest) + if err != nil { + return resp, err + } + return resp, nil +} + +// UnShareClusterWithValidation unshares a cluster with users and groups and validates the unshare. +func UnShareClusterWithValidation(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string) (*api.UnShareClusterResponse, error) { + backupDriver := Inst().Backup + unshareClusterRequest := &api.UnShareClusterRequest{ + OrgId: BackupOrgID, + ClusterRef: &api.ObjectRef{ + Name: clusterName, + Uid: clusterUid, + }, + // userid of the user(s) to share the cluster with + Users: users, + // group(s) to share the cluster with + Groups: groups, + } + resp, err := backupDriver.UnShareCluster(ctx, unshareClusterRequest) + if err != nil { + return resp, err + } + + err = ValidateUnShareCluster(ctx, clusterName, clusterUid, users, groups) + if err != nil { + return nil, err + } + return resp, nil +} + +// ValidateShareCluster validates that a cluster is shared with users and groups by validating collaborators list and inspecting cluster from user. +func ValidateShareCluster(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string) error { + backupDriver := Inst().Backup + userIds := []string{} + groupIds := []string{} + req := &api.ClusterInspectRequest{ + Name: clusterName, + OrgId: BackupOrgID, + Uid: clusterUid, + } + + // Inspect cluster to retrieve collaborators + clusterObject, err := backupDriver.InspectCluster(ctx, req) + if err != nil { + return err + } + + // Extract collaborator user and group IDs + clusterCollaborators := clusterObject.GetCluster().Ownership + for _, user := range clusterCollaborators.Collaborators { + userIds = append(userIds, user.Id) + } + for _, group := range clusterCollaborators.Groups { + groupIds = append(groupIds, group.Id) + } + + // Validate that users and groups are present in the collaborators list + for _, user := range users { + if !IsPresent(userIds, user) { + return fmt.Errorf("User with id [%s] is not present in the list of collaborators for cluster [%s]", user, clusterName) + } + } + for _, group := range groups { + if !IsPresent(groupIds, group) { + return fmt.Errorf("Group with id [%s] is not present in the list of collaborators for cluster [%s]", group, clusterName) + } + } + + // Validate that the entire list matches + if !AreStringSlicesEqual(userIds, users) { + return fmt.Errorf("User list [%v] does not match the collaborators users list [%v] for cluster [%s]", users, userIds, clusterName) + } + + if !AreStringSlicesEqual(groupIds, groups) { + return fmt.Errorf("Group list [%v] does not match the collaborators group list [%v] for cluster [%s]", groups, groupIds, clusterName) + } + + // Expand the users list with members from groups + for _, group := range groups { + usersFromGroup, _ := backup.GetMembersOfGroupWithID(group) + for _, user := range usersFromGroup { + if !IsPresent(users, user) { + users = append(users, user) + } + } + } + + // Validation that the cluster is accessible to shared users + errorChan := make(chan error, len(users)) + defer close(errorChan) + + var wg sync.WaitGroup + for _, user := range users { + wg.Add(1) + go func(user string) { + defer wg.Done() + nonAdminCtx, err := backup.GetNonAdminCtx(user, CommonPassword) + if err != nil { + errorChan <- err + return + } + log.Infof("Inspecting cluster [%s] with user [%s]", clusterName, user) + _, err = backupDriver.InspectCluster(nonAdminCtx, req) + if err != nil { + errorChan <- fmt.Errorf("User with id [%s] is not able to access the cluster [%s]", user, clusterName) + } + }(user) + } + + wg.Wait() + + // Collect errors from the channel + var errorMessages []string + for err := range errorChan { + errorMessages = append(errorMessages, err.Error()) + } + + if len(errorMessages) > 0 { + return fmt.Errorf("ValidateShareCluster Errors: %s", strings.Join(errorMessages, "; ")) + } + + return nil +} + +// ValidateShareClusterBackup validates that a cluster's backups are shared with users and groups by validating the backup's access. +func ValidateShareClusterBackup(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string) error { + backupDriver := Inst().Backup + clusterBackups, err := GetAllBackupsFromCluster(ctx, clusterName, clusterUid) + if err != nil { + return err + } + + // Combine users from groups into the users slice + for _, group := range groups { + usersFromGroup, _ := backup.GetMembersOfGroupWithID(group) + users = append(users, usersFromGroup...) + } + + errChan := make(chan error, len(clusterBackups)*len(users)) + + var wg sync.WaitGroup + for _, backupName := range clusterBackups { + for _, user := range users { + wg.Add(1) + go func(backupName, user string) { + defer wg.Done() + nonAdminCtx, err := backup.GetNonAdminCtx(user, CommonPassword) + if err != nil { + errChan <- err + return + } + backupInspectRequest := &api.BackupInspectRequest{ + OrgId: BackupOrgID, + Name: backupName, + } + log.Infof("Inspecting backup [%s] with user [%s]", backupName, user) + backupObj, err := backupDriver.InspectBackup(nonAdminCtx, backupInspectRequest) + if err != nil { + errChan <- fmt.Errorf("User with id [%s] is not able to access the backup [%s]", user, backupName) + return + } + + if backupObj.GetBackup().UserBackupshareAccess != api.BackupShare_Restorable { + errChan <- fmt.Errorf("User with id [%s] is expected to have Restore access for the backup [%s] but got [%s]", user, backupName, backupObj.GetBackup().UserBackupshareAccess) + } + }(backupName, user) + } + } + + wg.Wait() + close(errChan) + + var errorMessages []string + for err := range errChan { + errorMessages = append(errorMessages, err.Error()) + } + if len(errorMessages) > 0 { + return fmt.Errorf("ValidateShareClusterBackup Errors: %s", strings.Join(errorMessages, "; ")) + } + return nil +} + +// ValidateUnShareCluster validates that a cluster is unshared with users and groups by validating collaborators list and inspecting cluster from user. +func ValidateUnShareCluster(ctx context1.Context, clusterName string, clusterUid string, users []string, groups []string) error { + backupDriver := Inst().Backup + req := &api.ClusterInspectRequest{ + Name: clusterName, + OrgId: BackupOrgID, + Uid: clusterUid, + } + + // Inspect the cluster to get the collaborators list + clusterObject, err := backupDriver.InspectCluster(ctx, req) + if err != nil { + return err + } + + // Extract collaborator user and group IDs + collaboratorMap := make(map[string]bool) + for _, user := range clusterObject.GetCluster().Ownership.Collaborators { + collaboratorMap[user.Id] = true + } + for _, group := range clusterObject.GetCluster().Ownership.Groups { + collaboratorMap[group.Id] = true + } + + // Validate that users and groups are NOT present in the collaborators list + for _, user := range users { + if collaboratorMap[user] { + return fmt.Errorf("User with id [%s] is present in the list of collaborators for cluster [%s]", user, clusterName) + } + } + for _, group := range groups { + if collaboratorMap[group] { + return fmt.Errorf("Group with id [%s] is present in the list of collaborators for cluster [%s]", group, clusterName) + } + } + + // Combine users from groups into the users slice + for _, group := range groups { + usersFromGroup, _ := backup.GetMembersOfGroupWithID(group) + for _, user := range usersFromGroup { + if !IsPresent(users, user) { + users = append(users, user) + } + } + } + + // Validation that the cluster is not accessible to previously shared users + errorChan := make(chan error, len(users)) + var wg sync.WaitGroup + + for _, user := range users { + wg.Add(1) + go func(user string) { + defer wg.Done() + nonAdminCtx, err := backup.GetNonAdminCtx(user, CommonPassword) + if err != nil { + errorChan <- err + return + } + // If the user can access the cluster, it's an error + if _, err := backupDriver.InspectCluster(nonAdminCtx, req); err == nil { + errorChan <- fmt.Errorf("User with id [%s] is able to access the cluster [%s]", user, clusterName) + } + }(user) + } + + wg.Wait() + close(errorChan) + + var errorMessages []string + for err := range errorChan { + errorMessages = append(errorMessages, err.Error()) + } + + if len(errorMessages) > 0 { + return fmt.Errorf("ValidateUnShareCluster Errors: %s", strings.Join(errorMessages, "; ")) + } + + return nil +}