Skip to content

Commit 66c77f9

Browse files
committed
feat: add user as group / project owner
1 parent de03aae commit 66c77f9

File tree

8 files changed

+171
-31
lines changed

8 files changed

+171
-31
lines changed

internal/mirroring/groups.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ func (g *GitlabInstance) CreateGroupFromSource(sourceGroup *gitlab.Group, copyOp
105105
if err == nil {
106106
zap.L().Info("Group created", zap.String("group", destinationGroup.WebURL))
107107
g.AddGroup(destinationGroup)
108+
109+
// Claim ownership of the created group
110+
if err := g.ClaimOwnershipToGroup(destinationGroup); err != nil {
111+
zap.L().Warn("Failed to claim ownership of group", zap.String("group", destinationGroup.FullPath), zap.Error(err))
112+
}
108113
}
109114

110115
return destinationGroup, err
@@ -440,3 +445,20 @@ func (destinationGitlabInstance *GitlabInstance) syncGroupAttributes(sourceGroup
440445
}
441446
return nil
442447
}
448+
449+
// ClaimOwnershipToGroup adds the authenticated user as an owner to the specified group.
450+
// It uses the GitLab API to add the user as a group member with owner access level.
451+
func (g *GitlabInstance) ClaimOwnershipToGroup(group *gitlab.Group) error {
452+
zap.L().Debug("Claiming ownership of group", zap.String("group", group.FullPath), zap.Int("userID", g.UserID))
453+
454+
_, _, err := g.Gitlab.GroupMembers.AddGroupMember(group.ID, &gitlab.AddGroupMemberOptions{
455+
UserID: &g.UserID,
456+
AccessLevel: gitlab.Ptr(gitlab.AccessLevelValue(50)),
457+
})
458+
if err != nil {
459+
return fmt.Errorf("failed to add user as owner to group %s: %w", group.FullPath, err)
460+
}
461+
462+
zap.L().Info("Successfully claimed ownership of group", zap.String("group", group.FullPath))
463+
return nil
464+
}

internal/mirroring/groups_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,12 @@ func TestCreateGroups(t *testing.T) {
113113
})
114114
}
115115
}
116+
117+
func TestClaimOwnershipToGroup(t *testing.T) {
118+
_, gitlabInstance := setupTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
119+
120+
err := gitlabInstance.ClaimOwnershipToGroup(TEST_GROUP)
121+
if err != nil {
122+
t.Fatalf("expected no error, got %v", err)
123+
}
124+
}

internal/mirroring/helper_test.go

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ const (
1717
HEADER_ACCEPT = "application/json"
1818
)
1919

20+
// Helper functions for common HTTP responses
21+
func writeJSONResponse(w http.ResponseWriter, status int, body string) {
22+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
23+
w.WriteHeader(status)
24+
fmt.Fprint(w, body)
25+
}
26+
27+
func writeMethodNotAllowed(w http.ResponseWriter) {
28+
w.WriteHeader(http.StatusMethodNotAllowed)
29+
}
30+
2031
var (
2132
// TEST_PROJECT is a test project used in unit tests.
2233
TEST_PROJECT = &gitlab.Project{
@@ -379,6 +390,18 @@ func setupEmptyTestServer(t *testing.T, role string, instanceSize string) (*http
379390
server := httptest.NewServer(mux)
380391
t.Cleanup(server.Close)
381392

393+
// Add test handler for current user
394+
mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) {
395+
if r.Method != http.MethodGet {
396+
w.WriteHeader(http.StatusMethodNotAllowed)
397+
return
398+
}
399+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
400+
w.WriteHeader(http.StatusOK)
401+
// Return mock current user with ID 1
402+
fmt.Fprint(w, `{"id": 1, "username": "testuser", "name": "Test User", "state": "active"}`)
403+
})
404+
382405
gitlabInstance, err := NewGitlabInstance(&GitlabInstanceOpts{
383406
GitlabURL: server.URL,
384407
GitlabToken: "test-token",
@@ -405,30 +428,42 @@ func setupTestServer(t *testing.T, role string, instanceSize string) (*http.Serv
405428
server := httptest.NewServer(mux)
406429
t.Cleanup(server.Close)
407430

408-
gitlabInstance, err := NewGitlabInstance(&GitlabInstanceOpts{
409-
GitlabURL: server.URL,
410-
GitlabToken: "test-token",
411-
Role: role,
412-
InstanceSize: instanceSize,
413-
MaxRetries: 0,
414-
})
415-
416-
if err != nil {
417-
t.Fatalf("Failed to create client: %v", err)
418-
}
419-
420431
// Add test handlers for the projects and groups endpoints.
421432
setupTestProjects(mux)
422433
setupTestGroups(mux)
423434

424435
// Add test handlers for the GraphQL endpoint.
425436
setupTestGraphQL(mux)
426437

438+
// Add test handler for current user
439+
mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) {
440+
if r.Method != http.MethodGet {
441+
w.WriteHeader(http.StatusMethodNotAllowed)
442+
return
443+
}
444+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
445+
w.WriteHeader(http.StatusOK)
446+
// Return mock current user with ID 1
447+
fmt.Fprint(w, `{"id": 1, "username": "testuser", "name": "Test User", "state": "active"}`)
448+
})
449+
427450
// Catch-all handler for undefined routes
428451
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
429452
http.Error(w, fmt.Sprintf("Undefined route accessed: %s %s", r.Method, r.URL.Path), http.StatusNotFound)
430453
})
431454

455+
gitlabInstance, err := NewGitlabInstance(&GitlabInstanceOpts{
456+
GitlabURL: server.URL,
457+
GitlabToken: "test-token",
458+
Role: role,
459+
InstanceSize: instanceSize,
460+
MaxRetries: 0,
461+
})
462+
463+
if err != nil {
464+
t.Fatalf("Failed to create client: %v", err)
465+
}
466+
432467
return mux, gitlabInstance
433468
}
434469

@@ -476,15 +511,11 @@ func setupTestGroup(mux *http.ServeMux, group *gitlab.Group, stringResponse stri
476511
mux.HandleFunc(fmt.Sprintf("/api/v4/groups/%d", group.ID), func(w http.ResponseWriter, r *http.Request) {
477512
switch r.Method {
478513
case http.MethodGet:
479-
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
480-
w.WriteHeader(http.StatusOK)
481-
fmt.Fprint(w, stringResponse)
514+
writeJSONResponse(w, http.StatusOK, stringResponse)
482515
case http.MethodPut:
483-
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
484-
w.WriteHeader(http.StatusOK)
485-
fmt.Fprint(w, stringResponse)
516+
writeJSONResponse(w, http.StatusOK, stringResponse)
486517
default:
487-
w.WriteHeader(http.StatusMethodNotAllowed)
518+
writeMethodNotAllowed(w)
488519
return
489520
}
490521
})
@@ -499,6 +530,17 @@ func setupTestGroup(mux *http.ServeMux, group *gitlab.Group, stringResponse stri
499530
w.WriteHeader(http.StatusOK)
500531
w.Write([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) // PNG header
501532
})
533+
// Setup the add group member endpoint
534+
mux.HandleFunc(fmt.Sprintf("/api/v4/groups/%d/members", group.ID), func(w http.ResponseWriter, r *http.Request) {
535+
if r.Method != http.MethodPost {
536+
w.WriteHeader(http.StatusMethodNotAllowed)
537+
return
538+
}
539+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
540+
w.WriteHeader(http.StatusCreated)
541+
// Return a mock member response
542+
fmt.Fprint(w, `{"id": 1, "username": "testuser", "name": "Test User", "state": "active", "access_level": 50}`)
543+
})
502544
}
503545

504546
func setupTestGraphQL(mux *http.ServeMux) {
@@ -678,17 +720,24 @@ func setupTestProject(mux *http.ServeMux, project *gitlab.Project, stringRespons
678720
mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/issues/%d", project.ID, TEST_ISSUE.IID), func(w http.ResponseWriter, r *http.Request) {
679721
switch r.Method {
680722
case http.MethodGet:
681-
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
682-
w.WriteHeader(http.StatusOK)
683-
fmt.Fprint(w, TEST_ISSUE_STRING)
723+
writeJSONResponse(w, http.StatusOK, TEST_ISSUE_STRING)
684724
case http.MethodPut:
685-
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
686-
w.WriteHeader(http.StatusOK)
687-
fmt.Fprint(w, TEST_ISSUE_STRING)
725+
writeJSONResponse(w, http.StatusOK, TEST_ISSUE_STRING)
688726
default:
727+
writeMethodNotAllowed(w)
728+
return
729+
}
730+
})
731+
732+
mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/members", project.ID), func(w http.ResponseWriter, r *http.Request) {
733+
if r.Method != http.MethodPost {
689734
w.WriteHeader(http.StatusMethodNotAllowed)
690735
return
691736
}
737+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
738+
w.WriteHeader(http.StatusCreated)
739+
// Return a mock member response
740+
fmt.Fprint(w, `{"id": 1, "username": "testuser", "name": "Test User", "state": "active", "access_level": 50}`)
692741
})
693742
// Setup the get project issue notes response from the project ID and issue IID
694743
}

internal/mirroring/instance.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type GitlabInstance struct {
4545
PullMirrorAvailable bool
4646
// GitAuth is the HTTP authentication used for GitLab git over HTTP operations (only for non premium instances)
4747
GitAuth transport.AuthMethod
48+
// UserID is the ID of the authenticated user
49+
UserID int
4850
}
4951

5052
type GitlabInstanceOpts struct {
@@ -70,13 +72,20 @@ func NewGitlabInstance(initArgs *GitlabInstanceOpts) (*GitlabInstance, error) {
7072
return nil, err
7173
}
7274

75+
// Get the current user ID
76+
user, _, err := gitlabClient.Users.CurrentUser()
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to get current user: %w", err)
79+
}
80+
7381
gitlabInstance := &GitlabInstance{
7482
Gitlab: gitlabClient,
7583
Projects: make(map[string]*gitlab.Project),
7684
Groups: make(map[string]*gitlab.Group),
7785
Role: initArgs.Role,
7886
InstanceSize: initArgs.InstanceSize,
7987
GitAuth: helpers.BuildHTTPAuth("", initArgs.GitlabToken),
88+
UserID: user.ID,
8089
}
8190

8291
return gitlabInstance, nil

internal/mirroring/instance_test.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package mirroring
22

33
import (
4+
"fmt"
45
"net/http"
6+
"net/http/httptest"
57
"testing"
68

79
gitlab "gitlab.com/gitlab-org/api/client-go"
@@ -12,12 +14,25 @@ const (
1214
)
1315

1416
func TestNewGitlabInstance(t *testing.T) {
15-
gitlabURL := "https://gitlab.example.com"
16-
gitlabToken := "test-token"
17+
// Create a test server to mock the CurrentUser API
18+
mux := http.NewServeMux()
19+
server := httptest.NewServer(mux)
20+
t.Cleanup(server.Close)
21+
22+
// Add mock handler for current user
23+
mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) {
24+
if r.Method != http.MethodGet {
25+
w.WriteHeader(http.StatusMethodNotAllowed)
26+
return
27+
}
28+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
29+
w.WriteHeader(http.StatusOK)
30+
fmt.Fprint(w, `{"id": 1, "username": "testuser", "name": "Test User", "state": "active"}`)
31+
})
1732

1833
instance, err := NewGitlabInstance(&GitlabInstanceOpts{
19-
GitlabURL: gitlabURL,
20-
GitlabToken: gitlabToken,
34+
GitlabURL: server.URL,
35+
GitlabToken: "test-token",
2136
Role: ROLE_SOURCE,
2237
MaxRetries: 3,
2338
InstanceSize: INSTANCE_SIZE_SMALL,
@@ -37,6 +52,10 @@ func TestNewGitlabInstance(t *testing.T) {
3752
if instance.Groups == nil {
3853
t.Error("expected Groups map to be initialized")
3954
}
55+
56+
if instance.UserID != 1 {
57+
t.Errorf("expected UserID to be 1, got %d", instance.UserID)
58+
}
4059
}
4160

4261
func TestAddProject(t *testing.T) {
@@ -209,7 +228,7 @@ func TestIsVersionGreaterThanThreshold(t *testing.T) {
209228
mux, gitlabInstance := setupEmptyTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
210229
if !test.noApiResponse {
211230
mux.HandleFunc("/api/v4/metadata", func(w http.ResponseWriter, r *http.Request) {
212-
w.Header().Set("Content-Type", "application/json")
231+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
213232
w.WriteHeader(http.StatusOK)
214233
_, err := w.Write([]byte(`{"version": "` + test.version + `"}`))
215234
if err != nil {
@@ -275,7 +294,7 @@ func TestIsLicensePremium(t *testing.T) {
275294
mux, gitlabInstance := setupEmptyTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
276295
if !test.expectedError {
277296
mux.HandleFunc("/api/v4/license", func(w http.ResponseWriter, r *http.Request) {
278-
w.Header().Set("Content-Type", "application/json")
297+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
279298
w.WriteHeader(http.StatusOK)
280299
_, err := w.Write([]byte(`{"plan": "` + test.license + `"}`))
281300
if err != nil {

internal/mirroring/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func MirrorGitlabs(gitlabMirrorArgs *utils.ParserArgs) []error {
5555

5656
if err != nil {
5757
// Could not obtain a result from the destination GitLab instance, so we cannot proceed with the mirroring process.
58+
// TODO: have a non zero exit code in this case
5859
return []error{err}
5960
} else if pullMirrorAvailable {
6061
// Proceed with the pull mirroring process

internal/mirroring/projects.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ func (g *GitlabInstance) CreateProjectFromSource(sourceProject *gitlab.Project,
315315
if err == nil {
316316
zap.L().Info("Project created", zap.String("project", destinationProject.HTTPURLToRepo))
317317
g.AddProject(destinationProject)
318+
319+
// Claim ownership of the created project
320+
if err := g.ClaimOwnershipToProject(destinationProject); err != nil {
321+
zap.L().Warn("Failed to claim ownership of project", zap.String("project", destinationProject.PathWithNamespace), zap.Error(err))
322+
}
318323
}
319324

320325
return destinationProject, err
@@ -525,3 +530,20 @@ func (g *GitlabInstance) AddProjectToCICDCatalog(project *gitlab.Project) error
525530
_, err := g.Gitlab.GraphQL.Do(gitlab.GraphQLQuery{Query: query}, &response)
526531
return err
527532
}
533+
534+
// ClaimOwnershipToProject adds the authenticated user as an owner to the specified project.
535+
// It uses the GitLab API to add the user as a project member with owner access level.
536+
func (g *GitlabInstance) ClaimOwnershipToProject(project *gitlab.Project) error {
537+
zap.L().Debug("Claiming ownership of project", zap.String("project", project.PathWithNamespace), zap.Int("userID", g.UserID))
538+
539+
_, _, err := g.Gitlab.ProjectMembers.AddProjectMember(project.ID, &gitlab.AddProjectMemberOptions{
540+
UserID: &g.UserID,
541+
AccessLevel: gitlab.Ptr(gitlab.AccessLevelValue(50)),
542+
})
543+
if err != nil {
544+
return fmt.Errorf("failed to add user as owner to project %s: %w", project.PathWithNamespace, err)
545+
}
546+
547+
zap.L().Info("Successfully claimed ownership of project", zap.String("project", project.PathWithNamespace))
548+
return nil
549+
}

internal/mirroring/projects_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,12 @@ func TestAddProjectToCICDCatalog(t *testing.T) {
283283
}
284284
})
285285
}
286+
287+
func TestClaimOwnershipToProject(t *testing.T) {
288+
_, gitlabInstance := setupTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
289+
290+
err := gitlabInstance.ClaimOwnershipToProject(TEST_PROJECT)
291+
if err != nil {
292+
t.Fatalf("expected no error, got %v", err)
293+
}
294+
}

0 commit comments

Comments
 (0)