Repository: diggerhq/digger
Pull request #372 (2023-06-15 13:53:46)

Support org level rbac


Patch:
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index 85918c20..5dfcb2ed 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -42,6 +42,12 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
 		reportErrorAndExit(githubActor, "GITHUB_TOKEN is not defined", 1)
 	}
 
+	diggerGitHubToken := os.Getenv("DIGGER_GITHUB_TOKEN")
+	if diggerGitHubToken != "" {
+		fmt.Println("GITHUB_TOKEN has been overridden with DIGGER_GITHUB_TOKEN")
+		ghToken = diggerGitHubToken
+	}
+
 	ghContext := os.Getenv("GITHUB_CONTEXT")
 	if ghContext == "" {
 		reportErrorAndExit(githubActor, "GITHUB_CONTEXT is not defined", 2)
diff --git a/pkg/azure/azure.go b/pkg/azure/azure.go
index 9caffbf8..d5911390 100644
--- a/pkg/azure/azure.go
+++ b/pkg/azure/azure.go
@@ -135,6 +135,10 @@ type AzureReposService struct {
 	RepositoryId string
 }
 
+func (a *AzureReposService) GetUserTeams(organisation string, user string) ([]string, error) {
+	return make([]string, 0), nil
+}
+
 func (a *AzureReposService) GetChangedFiles(prNumber int) ([]string, error) {
 
 	pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
diff --git a/pkg/ci/ci.go b/pkg/ci/ci.go
index e0d7dd33..d61c5471 100644
--- a/pkg/ci/ci.go
+++ b/pkg/ci/ci.go
@@ -9,4 +9,5 @@ type CIService interface {
 	MergePullRequest(prNumber int) error
 	IsMergeable(prNumber int) (bool, error)
 	IsClosed(prNumber int) (bool, error)
+	GetUserTeams(organisation string, user string) ([]string, error)
 }
diff --git a/pkg/core/policy/policy.go b/pkg/core/policy/policy.go
index eedf9811..6a991aaa 100644
--- a/pkg/core/policy/policy.go
+++ b/pkg/core/policy/policy.go
@@ -5,5 +5,5 @@ type Provider interface {
 }
 
 type Checker interface {
-	Check(namespace string, projectname string, input interface{}) (bool, error)
+	Check(organisation string, namespace string, projectname string, input interface{}) (bool, error)
 }
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index 61673605..a1026f1e 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -74,11 +74,25 @@ func RunCommandsPerProject(
 	accumulatePlans := os.Getenv("ACCUMULATE_PLANS") == "true"
 	appliesPerProject := make(map[string]bool)
 	plansToPublish := make([]string, 0)
+
+	organisation := strings.Split(projectNamespace, "/")[0]
+	teams, err := ciService.GetUserTeams(organisation, requestedBy)
+	if err != nil {
+		fmt.Printf("Error while fetching user teams for CI service: %v", err)
+	}
+
 	for _, projectCommands := range commandsPerProject {
 		for _, command := range projectCommands.Commands {
-			policyInput := map[string]interface{}{"user": requestedBy, "action": command}
 
-			allowedToPerformCommand, err := policyChecker.Check(projectNamespace, projectCommands.ProjectName, policyInput)
+			policyInput := map[string]interface{}{
+				"user":         requestedBy,
+				"organisation": organisation,
+				"teams":        teams,
+				"action":       command,
+				"project":      projectCommands.ProjectName,
+			}
+
+			allowedToPerformCommand, err := policyChecker.Check(organisation, projectNamespace, projectCommands.ProjectName, policyInput)
 
 			if err != nil {
 				return false, false, fmt.Errorf("error checking policy: %v", err)
@@ -90,6 +104,7 @@ func RunCommandsPerProject(
 				if err != nil {
 					log.Printf("Error publishing comment: %v", err)
 				}
+				log.Println(msg)
 				return false, false, errors.New(msg)
 			}
 
diff --git a/pkg/github/github.go b/pkg/github/github.go
index e14c25a9..c7f85202 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -30,10 +30,29 @@ type GithubService struct {
 	Owner    string
 }
 
+func (svc *GithubService) GetUserTeams(organisation string, user string) ([]string, error) {
+	teamsResponse, _, err := svc.Client.Teams.ListTeams(context.Background(), organisation, nil)
+	if err != nil {
+		log.Fatalf("Failed to list github teams: %v", err)
+	}
+	var teams []string
+	for _, team := range teamsResponse {
+		teamMembers, _, _ := svc.Client.Teams.ListTeamMembersBySlug(context.Background(), organisation, *team.Slug, nil)
+		for _, member := range teamMembers {
+			if *member.Login == user {
+				teams = append(teams, *team.Name)
+				break
+			}
+		}
+	}
+
+	return teams, nil
+}
+
 func (svc *GithubService) GetChangedFiles(prNumber int) ([]string, error) {
 	files, _, err := svc.Client.PullRequests.ListFiles(context.Background(), svc.Owner, svc.RepoName, prNumber, nil)
 	if err != nil {
-		log.Fatalf("error getting pull request: %v", err)
+		log.Fatalf("error getting pull request files: %v", err)
 	}
 
 	fileNames := make([]string, len(files))
@@ -203,9 +222,10 @@ func ConvertGithubEventToCommands(event models.Event, impactedProjects []configu
 		}
 
 		diggerCommand := strings.ToLower(event.Comment.Body)
+		diggerCommand = strings.TrimSpace(diggerCommand)
 
 		for _, command := range supportedCommands {
-			if strings.Contains(diggerCommand, command) {
+			if strings.HasPrefix(diggerCommand, command) {
 				for _, project := range runForProjects {
 					workflow, ok := workflows[project.Workflow]
 					if !ok {
diff --git a/pkg/gitlab/gitlab.go b/pkg/gitlab/gitlab.go
index 32394feb..ca48ff8e 100644
--- a/pkg/gitlab/gitlab.go
+++ b/pkg/gitlab/gitlab.go
@@ -103,6 +103,7 @@ func ProcessGitLabEvent(gitlabContext *GitLabContext, diggerConfig *configuratio
 	switch gitlabContext.EventType {
 	case MergeRequestComment:
 		diggerCommand := strings.ToLower(gitlabContext.DiggerCommand)
+		diggerCommand = strings.TrimSpace(diggerCommand)
 		requestedProject := utils.ParseProjectName(diggerCommand)
 
 		if requestedProject == "" {
@@ -145,6 +146,10 @@ func (gitlabService GitLabService) GetChangedFiles(mergeRequestId int) ([]string
 	return fileNames, nil
 }
 
+func (gitlabService GitLabService) GetUserTeams(organisation string, user string) ([]string, error) {
+	return make([]string, 0), nil
+}
+
 func (gitlabService GitLabService) PublishComment(mergeRequestID int, comment string) error {
 	discussionId := gitlabService.Context.DiscussionID
 	projectId := *gitlabService.Context.ProjectId
@@ -314,6 +319,7 @@ func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContex
 		}
 
 		diggerCommand := strings.ToLower(gitLabContext.DiggerCommand)
+		diggerCommand = strings.TrimSpace(diggerCommand)
 		for _, command := range supportedCommands {
 			if strings.Contains(diggerCommand, command) {
 				for _, project := range runForProjects {
diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go
index dda5ba35..06ccc734 100644
--- a/pkg/policy/policy.go
+++ b/pkg/policy/policy.go
@@ -2,15 +2,16 @@ package policy
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"github.com/open-policy-agent/opa/rego"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strings"
 )
 
 type PolicyProvider interface {
-	GetPolicy(namespace string, projectname string) (string, error)
+	GetPolicy(organisation string, namespace string, projectname string) (string, error)
 }
 
 type DiggerHttpPolicyProvider struct {
@@ -22,43 +23,91 @@ type DiggerHttpPolicyProvider struct {
 type NoOpPolicyChecker struct {
 }
 
-func (p NoOpPolicyChecker) Check(_ string, _ string, _ interface{}) (bool, error) {
+func (p NoOpPolicyChecker) Check(_ string, _ string, _ string, _ interface{}) (bool, error) {
 	return true, nil
 }
 
-func (p *DiggerHttpPolicyProvider) GetPolicy(namespace string, projectName string) (string, error) {
-	// fetch RBAC policies from Digger API
+func (p *DiggerHttpPolicyProvider) getPolicyForOrganisation(organisation string) (string, *http.Response, error) {
+
+	req, err := http.NewRequest("GET", p.DiggerHost+"/orgs/"+organisation+"/access-policy", nil)
+	if err != nil {
+		return "", nil, err
+	}
+	req.Header.Add("Authorization", "Bearer "+p.AuthToken)
+
+	resp, err := p.HttpClient.Do(req)
+	if err != nil {
+		return "", nil, err
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", resp, nil
+	}
+	return string(body), resp, nil
+}
+
+func (p *DiggerHttpPolicyProvider) getPolicyForNamespace(namespace string, projectName string) (string, *http.Response, error) {
+
+	// fetch RBAC policies for projectfrom Digger API
 	namespace = strings.ReplaceAll(namespace, "/", "-")
 	req, err := http.NewRequest("GET", p.DiggerHost+"/repos/"+namespace+"/projects/"+projectName+"/access-policy", nil)
+
 	if err != nil {
-		return "", err
+		return "", nil, err
 	}
 	req.Header.Add("Authorization", "Bearer "+p.AuthToken)
 
 	resp, err := p.HttpClient.Do(req)
 	if err != nil {
-		return "", err
+		return "", nil, err
 	}
 	defer resp.Body.Close()
 
-	body, err := ioutil.ReadAll(resp.Body)
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", resp, nil
+	}
+	return string(body), resp, nil
+
+}
+
+// GetPolicy fetches policy for entire organisation, if not found for organisation then it will fetch it for project
+func (p *DiggerHttpPolicyProvider) GetPolicy(organisation string, namespace string, projectName string) (string, error) {
+	content, resp, err := p.getPolicyForOrganisation(organisation)
 	if err != nil {
 		return "", err
 	}
-	return string(body), nil
+	if resp.StatusCode == 200 {
+		return content, nil
+	} else if resp.StatusCode == 404 {
+		content, resp, err := p.getPolicyForNamespace(namespace, projectName)
+		if err != nil {
+			return "", err
+		}
+		if resp.StatusCode == 200 {
+			return content, nil
+		} else {
+			return "", errors.New(fmt.Sprintf("unexpected response while fetching namespace policy: %v, code %v", content, resp.StatusCode))
+		}
+	} else {
+		return "", errors.New(fmt.Sprintf("unexpected response while fetching org policy: %v code %v", content, resp.StatusCode))
+	}
 }
 
 type DiggerPolicyChecker struct {
 	PolicyProvider PolicyProvider
 }
 
-func (p DiggerPolicyChecker) Check(namespace string, projectName string, input interface{}) (bool, error) {
-	policy, err := p.PolicyProvider.GetPolicy(namespace, projectName)
+func (p DiggerPolicyChecker) Check(organisation string, namespace string, projectName string, input interface{}) (bool, error) {
+	policy, err := p.PolicyProvider.GetPolicy(organisation, namespace, projectName)
 
 	if err != nil {
 		return false, err
 	}
 	ctx := context.Background()
+	fmt.Printf("DEBUG: passing the following input policy: %v ||| text: %v", input, policy)
 	query, err := rego.New(
 		rego.Query("data.digger.allow"),
 		rego.Module("digger", policy),
diff --git a/pkg/utils/mocks.go b/pkg/utils/mocks.go
index 0c3c3410..077e4f79 100644
--- a/pkg/utils/mocks.go
+++ b/pkg/utils/mocks.go
@@ -42,7 +42,7 @@ func (lock *MockLock) GetLock(resource string) (*int, error) {
 type MockPolicyChecker struct {
 }
 
-func (t MockPolicyChecker) Check(_ string, _ string, _ interface{}) (bool, error) {
+func (t MockPolicyChecker) Check(organisation string, namespace string, projectname string, input interface{}) (bool, error) {
 	return false, nil
 }
 
@@ -50,6 +50,10 @@ type MockPullRequestManager struct {
 	ChangedFiles []string
 }
 
+func (t MockPullRequestManager) GetUserTeams(organisation string, user string) ([]string, error) {
+	return []string{}, nil
+}
+
 func (t MockPullRequestManager) GetChangedFiles(prNumber int) ([]string, error) {
 	return t.ChangedFiles, nil
 }
diff --git a/pkg/utils/pullrequestmanager_mock.go b/pkg/utils/pullrequestmanager_mock.go
index ad80df8e..60cabdd4 100644
--- a/pkg/utils/pullrequestmanager_mock.go
+++ b/pkg/utils/pullrequestmanager_mock.go
@@ -4,6 +4,10 @@ type MockGithubPullrequestManager struct {
 	commands []string
 }
 
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetUserTeams(organisation string, user string) ([]string, error) {
+	return []string{}, nil
+}
+
 func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetChangedFiles(prNumber int) ([]string, error) {
 	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "GetChangedFiles")
 	return nil, nil


Test patch:
diff --git a/pkg/digger/digger_test.go b/pkg/digger/digger_test.go
index daf597c4..397a921f 100644
--- a/pkg/digger/digger_test.go
+++ b/pkg/digger/digger_test.go
@@ -55,6 +55,10 @@ type MockPRManager struct {
 	Commands []RunInfo
 }
 
+func (m *MockPRManager) GetUserTeams(organisation string, user string) ([]string, error) {
+	return []string{}, nil
+}
+
 func (m *MockPRManager) GetChangedFiles(prNumber int) ([]string, error) {
 	m.Commands = append(m.Commands, RunInfo{"GetChangedFiles", strconv.Itoa(prNumber), time.Now()})
 	return []string{}, nil
diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go
index 64f95fc5..501963f3 100644
--- a/pkg/policy/policy_test.go
+++ b/pkg/policy/policy_test.go
@@ -7,7 +7,7 @@ import (
 type OpaExamplePolicyProvider struct {
 }
 
-func (s *OpaExamplePolicyProvider) GetPolicy(_ string, _ string) (string, error) {
+func (s *OpaExamplePolicyProvider) GetPolicy(_ string, _ string, _ string) (string, error) {
 	return "package digger\n" +
 		"\n" +
 		"# user-role assignments\n" +
@@ -43,7 +43,7 @@ func (s *OpaExamplePolicyProvider) GetPolicy(_ string, _ string) (string, error)
 type DiggerExamplePolicyProvider struct {
 }
 
-func (s *DiggerExamplePolicyProvider) GetPolicy(_ string, _ string) (string, error) {
+func (s *DiggerExamplePolicyProvider) GetPolicy(_ string, _ string, _ string) (string, error) {
 	return "package digger\n" +
 		"\n" +
 		"user_permissions := {\n" +
@@ -62,7 +62,7 @@ func (s *DiggerExamplePolicyProvider) GetPolicy(_ string, _ string) (string, err
 type DiggerExamplePolicyProvider2 struct {
 }
 
-func (s *DiggerExamplePolicyProvider2) GetPolicy(_ string, _ string) (string, error) {
+func (s *DiggerExamplePolicyProvider2) GetPolicy(_ string, _ string, _ string) (string, error) {
 	return "package digger\n" +
 		"\n" +
 		"user_permissions := {\n" +
@@ -89,11 +89,12 @@ func TestDiggerPolicyChecker_Check(t *testing.T) {
 		input interface{}
 	}
 	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		want    bool
-		wantErr bool
+		name         string
+		organisation string
+		fields       fields
+		args         args
+		want         bool
+		wantErr      bool
 	}{
 		{
 			name: "test opa example",
@@ -102,9 +103,10 @@ func TestDiggerPolicyChecker_Check(t *testing.T) {
 			},
 			args: args{
 				input: map[string]interface{}{
-					"user":   "alice",
-					"action": "read",
-					"object": "server123",
+					"organisation": "diggerhq",
+					"user":         "alice",
+					"action":       "read",
+					"object":       "server123",
 				},
 			},
 			want:    true,
@@ -173,7 +175,7 @@ func TestDiggerPolicyChecker_Check(t *testing.T) {
 			p := &DiggerPolicyChecker{
 				PolicyProvider: tt.fields.PolicyProvider,
 			}
-			got, err := p.Check(tt.name, tt.name, tt.args.input)
+			got, err := p.Check(tt.organisation, tt.name, tt.name, tt.args.input)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("DiggerPolicyChecker.Check() error = %v, wantErr %v", err, tt.wantErr)
 				return