Skip to content

Commit

Permalink
chore: migrate to project iam policy (#12124)
Browse files Browse the repository at this point in the history
* chore: add user_group table

* chore: update

* chore: update

* chore: update error

* fix: nil pointer in pg schema state

* chore: user group api

* Update proto/v1/user_group.proto

Co-authored-by: Danny Xu <98006139+d-bytebase@users.noreply.github.com>

* Update proto/v1/user_group.proto

Co-authored-by: Danny Xu <98006139+d-bytebase@users.noreply.github.com>

* Update proto/v1/user_group.proto

Co-authored-by: Danny Xu <98006139+d-bytebase@users.noreply.github.com>

* chore: update

* fix: lint

* feat: implement group

* fix: eslint

* chore: update iam proto

* chore: update

* chore: update

* chore: update

* fix: lint

* fix: lint

* chore: migrate to project iam policy

* chore: check for group

* chore: update

---------

Co-authored-by: Danny Xu <98006139+d-bytebase@users.noreply.github.com>
  • Loading branch information
ecmadao and d-bytebase committed May 26, 2024
1 parent 9a12042 commit adea41a
Show file tree
Hide file tree
Showing 22 changed files with 379 additions and 1,075 deletions.
4 changes: 2 additions & 2 deletions backend/api/v1/branch_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -844,11 +844,11 @@ func (s *BranchService) checkProtectionRules(ctx context.Context, project *store
return nil
}

policy, err := s.store.GetProjectPolicy(ctx, &store.GetProjectPolicyMessage{ProjectID: &project.ResourceID})
policy, err := s.store.GetProjectIamPolicy(ctx, project.UID)
if err != nil {
return err
}
roles, err := utils.GetUserFormattedRolesMap(user, policy)
roles, err := utils.GetUserFormattedRolesMap(ctx, s.store, user, policy)
if err != nil {
return errors.Wrapf(err, "failed to get user roles")
}
Expand Down
35 changes: 14 additions & 21 deletions backend/api/v1/database_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/bytebase/bytebase/backend/runner/schemasync"
"github.com/bytebase/bytebase/backend/store"
"github.com/bytebase/bytebase/backend/store/model"
"github.com/bytebase/bytebase/backend/utils"
storepb "github.com/bytebase/bytebase/proto/generated-go/store"
v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
)
Expand Down Expand Up @@ -314,40 +315,32 @@ func filterDatabasesV2(ctx context.Context, s *store.Store, iamManager *iam.Mana
}

func filterProjectDatabasesV2(ctx context.Context, s *store.Store, iamManager *iam.Manager, user *store.UserMessage, projectID string, databases []*store.DatabaseMessage, needPermission iam.Permission) ([]*store.DatabaseMessage, error) {
policy, err := s.GetProjectPolicy(ctx, &store.GetProjectPolicyMessage{
ProjectID: &projectID,
project, err := s.GetProjectV2(ctx, &store.FindProjectMessage{
ResourceID: &projectID,
})
if err != nil {
return nil, err
}
if project == nil {
return nil, errors.Errorf("cannot found project %s", projectID)
}

policy, err := s.GetProjectIamPolicy(ctx, project.UID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get project policy for project %q", projectID)
}

expressionDBsFromAllRoles := make(map[string]bool)
for _, binding := range policy.Bindings {
hasUser := false
for _, member := range binding.Members {
if member.ID == user.ID || member.Email == api.AllUsers {
hasUser = true
break
}
}
if !hasUser {
continue
}
bindings := utils.GetUserIAMPolicyBindings(ctx, s, user, policy)

permissions, err := iamManager.GetPermissions(ctx, common.FormatRole(binding.Role.String()))
for _, binding := range bindings {
permissions, err := iamManager.GetPermissions(ctx, binding.Role)
if err != nil {
return nil, errors.Wrapf(err, "failed to get permissions")
}
if !slices.Contains(permissions, needPermission) {
continue
}
ok, err := common.EvalBindingCondition(binding.Condition.GetExpression(), time.Now())
if err != nil {
return nil, errors.Wrapf(err, "failed to eval binding condition")
}
if !ok {
continue
}

expressionDBs := getDatabasesFromExpression(binding.Condition.Expression)
if len(expressionDBs) == 0 {
Expand Down
12 changes: 6 additions & 6 deletions backend/api/v1/issue_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,12 +874,12 @@ func (s *IssueService) ApproveIssue(ctx context.Context, request *v1pb.ApproveIs
return nil, status.Errorf(codes.Internal, "failed to find user by id %v", principalID)
}

policy, err := s.store.GetProjectPolicy(ctx, &store.GetProjectPolicyMessage{UID: &issue.Project.UID})
policy, err := s.store.GetProjectIamPolicy(ctx, issue.Project.UID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get project policy, error: %v", err)
}

canApprove, err := isUserReviewer(step, user, policy)
canApprove, err := isUserReviewer(ctx, s.store, step, user, policy)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check if principal can approve step, error: %v", err)
}
Expand Down Expand Up @@ -1122,12 +1122,12 @@ func (s *IssueService) RejectIssue(ctx context.Context, request *v1pb.RejectIssu
return nil, status.Errorf(codes.Internal, "failed to find user by id %v", principalID)
}

policy, err := s.store.GetProjectPolicy(ctx, &store.GetProjectPolicyMessage{UID: &issue.Project.UID})
policy, err := s.store.GetProjectIamPolicy(ctx, issue.Project.UID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get project policy, error: %v", err)
}

canApprove, err := isUserReviewer(step, user, policy)
canApprove, err := isUserReviewer(ctx, s.store, step, user, policy)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check if principal can reject step, error: %v", err)
}
Expand Down Expand Up @@ -1835,7 +1835,7 @@ func canRequestIssue(issueCreator *store.UserMessage, user *store.UserMessage) b
return issueCreator.ID == user.ID
}

func isUserReviewer(step *storepb.ApprovalStep, user *store.UserMessage, policy *store.IAMPolicyMessage) (bool, error) {
func isUserReviewer(ctx context.Context, stores *store.Store, step *storepb.ApprovalStep, user *store.UserMessage, policy *storepb.ProjectIamPolicy) (bool, error) {
if len(step.Nodes) != 1 {
return false, errors.Errorf("expecting one node but got %v", len(step.Nodes))
}
Expand All @@ -1847,7 +1847,7 @@ func isUserReviewer(step *storepb.ApprovalStep, user *store.UserMessage, policy
return false, errors.Errorf("expecting ANY_IN_GROUP node type but got %v", node.Type)
}

roles, err := utils.GetUserFormattedRolesMap(user, policy)
roles, err := utils.GetUserFormattedRolesMap(ctx, stores, user, policy)
if err != nil {
return false, errors.Wrapf(err, "failed to get user roles")
}
Expand Down
152 changes: 1 addition & 151 deletions backend/api/v1/issue_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (

"github.com/stretchr/testify/require"

api "github.com/bytebase/bytebase/backend/legacyapi"
"github.com/bytebase/bytebase/backend/store"
storepb "github.com/bytebase/bytebase/proto/generated-go/store"
v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
)
Expand Down Expand Up @@ -39,152 +37,4 @@ func TestConvertToApprovalNode(t *testing.T) {
}
}

func TestCanUserApproveStep(t *testing.T) {
tests := []struct {
step *storepb.ApprovalStep
user *store.UserMessage
policy *store.IAMPolicyMessage
want bool
}{
{
step: &storepb.ApprovalStep{
Type: storepb.ApprovalStep_ANY,
Nodes: []*storepb.ApprovalNode{
{
Type: storepb.ApprovalNode_ANY_IN_GROUP,
Payload: &storepb.ApprovalNode_GroupValue_{
GroupValue: storepb.ApprovalNode_WORKSPACE_DBA,
},
},
},
},
user: &store.UserMessage{
ID: 1,
Roles: []api.Role{api.WorkspaceMember},
},
policy: &store.IAMPolicyMessage{
Bindings: []*store.PolicyBinding{
{
Role: api.WorkspaceMember,
Members: []*store.UserMessage{
{
ID: 1,
Role: api.WorkspaceMember,
},
},
},
},
},
want: false,
},
{
step: &storepb.ApprovalStep{
Type: storepb.ApprovalStep_ANY,
Nodes: []*storepb.ApprovalNode{
{
Type: storepb.ApprovalNode_ANY_IN_GROUP,
Payload: &storepb.ApprovalNode_GroupValue_{
GroupValue: storepb.ApprovalNode_WORKSPACE_DBA,
},
},
},
},
user: &store.UserMessage{
ID: 1,
Roles: []api.Role{api.WorkspaceDBA},
},
policy: &store.IAMPolicyMessage{
Bindings: []*store.PolicyBinding{
{
Role: api.WorkspaceMember,
Members: []*store.UserMessage{
{
ID: 1,
Role: api.WorkspaceMember,
},
},
},
},
},
want: true,
},
{
step: &storepb.ApprovalStep{
Type: storepb.ApprovalStep_ANY,
Nodes: []*storepb.ApprovalNode{
{
Type: storepb.ApprovalNode_ANY_IN_GROUP,
Payload: &storepb.ApprovalNode_Role{
Role: "roles/ProjectDBA",
},
},
},
},
user: &store.UserMessage{
ID: 1,
Roles: []api.Role{api.WorkspaceDBA},
},
policy: &store.IAMPolicyMessage{
Bindings: []*store.PolicyBinding{
{
Role: api.WorkspaceMember,
Members: []*store.UserMessage{
{
ID: 1,
Role: api.WorkspaceMember,
},
},
},
},
},
want: false,
},
{
step: &storepb.ApprovalStep{
Type: storepb.ApprovalStep_ANY,
Nodes: []*storepb.ApprovalNode{
{
Type: storepb.ApprovalNode_ANY_IN_GROUP,
Payload: &storepb.ApprovalNode_Role{
Role: "roles/ProjectDBA",
},
},
},
},
user: &store.UserMessage{
ID: 1,
Roles: []api.Role{api.WorkspaceDBA},
},
policy: &store.IAMPolicyMessage{
Bindings: []*store.PolicyBinding{
{
Role: api.WorkspaceMember,
Members: []*store.UserMessage{
{
ID: 1,
Role: api.WorkspaceMember,
},
},
},
{
Role: "ProjectDBA",
Members: []*store.UserMessage{
{
ID: 1,
Role: api.WorkspaceMember,
},
},
},
},
},
want: true,
},
}

a := require.New(t)
for _, test := range tests {
got, err := isUserReviewer(test.step, test.user, test.policy)
a.NoError(err)
a.Equal(test.want, got)
}
}
// TODO(p0ny): update tests for isUserReviewer
Loading

0 comments on commit adea41a

Please sign in to comment.