diff --git a/app/cli/internal/action/membership_list.go b/app/cli/internal/action/membership_list.go index 1a3fb9d02..aafa0df16 100644 --- a/app/cli/internal/action/membership_list.go +++ b/app/cli/internal/action/membership_list.go @@ -162,10 +162,11 @@ func pbMembershipItemToAction(in *pb.OrgMembershipItem) *MembershipItem { type Role string const ( - RoleAdmin Role = "admin" - RoleOwner Role = "owner" - RoleViewer Role = "viewer" - RoleMember Role = "member" + RoleAdmin Role = "admin" + RoleOwner Role = "owner" + RoleViewer Role = "viewer" + RoleMember Role = "member" + RoleContributor Role = "contributor" ) type Roles []Role @@ -175,6 +176,7 @@ var AvailableRoles = Roles{ RoleOwner, RoleViewer, RoleMember, + RoleContributor, } func (roles Roles) String() string { @@ -195,6 +197,8 @@ func pbRoleToString(role pb.MembershipRole) Role { return RoleOwner case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER: return RoleMember + case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_CONTRIBUTOR: + return RoleContributor } return "" } @@ -209,6 +213,8 @@ func stringToPbRole(role Role) pb.MembershipRole { return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER case RoleMember: return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER + case RoleContributor: + return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_CONTRIBUTOR } return pb.MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED } diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index 7fb519a63..4eb15d017 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -100,11 +100,12 @@ func (RunStatus) EnumDescriptor() ([]byte, []int) { type MembershipRole int32 const ( - MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED MembershipRole = 0 - MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER MembershipRole = 1 - MembershipRole_MEMBERSHIP_ROLE_ORG_ADMIN MembershipRole = 2 - MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER MembershipRole = 3 - MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER MembershipRole = 4 + MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED MembershipRole = 0 + MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER MembershipRole = 1 + MembershipRole_MEMBERSHIP_ROLE_ORG_ADMIN MembershipRole = 2 + MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER MembershipRole = 3 + MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER MembershipRole = 4 + MembershipRole_MEMBERSHIP_ROLE_ORG_CONTRIBUTOR MembershipRole = 5 ) // Enum value maps for MembershipRole. @@ -115,13 +116,15 @@ var ( 2: "MEMBERSHIP_ROLE_ORG_ADMIN", 3: "MEMBERSHIP_ROLE_ORG_OWNER", 4: "MEMBERSHIP_ROLE_ORG_MEMBER", + 5: "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR", } MembershipRole_value = map[string]int32{ - "MEMBERSHIP_ROLE_UNSPECIFIED": 0, - "MEMBERSHIP_ROLE_ORG_VIEWER": 1, - "MEMBERSHIP_ROLE_ORG_ADMIN": 2, - "MEMBERSHIP_ROLE_ORG_OWNER": 3, - "MEMBERSHIP_ROLE_ORG_MEMBER": 4, + "MEMBERSHIP_ROLE_UNSPECIFIED": 0, + "MEMBERSHIP_ROLE_ORG_VIEWER": 1, + "MEMBERSHIP_ROLE_ORG_ADMIN": 2, + "MEMBERSHIP_ROLE_ORG_OWNER": 3, + "MEMBERSHIP_ROLE_ORG_MEMBER": 4, + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR": 5, } ) @@ -2968,7 +2971,7 @@ var file_controlplane_v1_response_messages_proto_rawDesc = []byte{ 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, - 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xaf, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, + 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xd4, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, @@ -2979,42 +2982,44 @@ var file_controlplane_v1_response_messages_proto_rawDesc = []byte{ 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, - 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x10, 0x04, 0x2a, 0x60, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, - 0x6f, 0x77, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x1c, 0x41, - 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, 0x0a, - 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x01, 0x1a, - 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x6d, 0x0a, 0x12, 0x46, - 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x41, 0x75, 0x74, 0x68, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, - 0x55, 0x54, 0x48, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x21, 0x46, 0x45, 0x44, 0x45, 0x52, - 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, - 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x1a, 0x04, - 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x84, 0x01, 0x0a, 0x19, 0x55, - 0x73, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x4e, 0x6f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, - 0x68, 0x69, 0x70, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x29, 0x55, 0x53, 0x45, 0x52, - 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, - 0x48, 0x49, 0x50, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x32, 0x0a, 0x28, 0x55, 0x53, 0x45, 0x52, 0x5f, - 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, - 0x49, 0x50, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, - 0x4f, 0x52, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, - 0x03, 0x2a, 0x80, 0x01, 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x4f, 0x66, 0x4f, 0x72, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, - 0x28, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, - 0x5f, 0x4f, 0x46, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x31, 0x0a, 0x27, 0x55, - 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, - 0x46, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, - 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, - 0xa0, 0x45, 0xf4, 0x03, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, - 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x4d, 0x45, 0x4d, + 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, + 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x4f, 0x52, 0x10, 0x05, 0x2a, 0x60, + 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x26, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4c, 0x49, + 0x53, 0x54, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, + 0x2a, 0x6d, 0x0a, 0x12, 0x46, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, + 0x54, 0x45, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x21, + 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, + 0x44, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, + 0x84, 0x01, 0x0a, 0x19, 0x55, 0x73, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x4e, 0x6f, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, + 0x29, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, + 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x32, 0x0a, 0x28, + 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, + 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, + 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, + 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x80, 0x01, 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x4e, + 0x6f, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x4f, 0x72, 0x67, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x28, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, + 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x31, 0x0a, 0x27, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, + 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, + 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, + 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, + 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index a6cdba957..ef44d6c61 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -254,6 +254,7 @@ enum MembershipRole { MEMBERSHIP_ROLE_ORG_ADMIN = 2; MEMBERSHIP_ROLE_ORG_OWNER = 3; MEMBERSHIP_ROLE_ORG_MEMBER = 4; + MEMBERSHIP_ROLE_ORG_CONTRIBUTOR = 5; } message OrgItem { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index 5c579c249..7838dc722 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -74,6 +74,7 @@ export enum MembershipRole { MEMBERSHIP_ROLE_ORG_ADMIN = 2, MEMBERSHIP_ROLE_ORG_OWNER = 3, MEMBERSHIP_ROLE_ORG_MEMBER = 4, + MEMBERSHIP_ROLE_ORG_CONTRIBUTOR = 5, UNRECOGNIZED = -1, } @@ -94,6 +95,9 @@ export function membershipRoleFromJSON(object: any): MembershipRole { case 4: case "MEMBERSHIP_ROLE_ORG_MEMBER": return MembershipRole.MEMBERSHIP_ROLE_ORG_MEMBER; + case 5: + case "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR": + return MembershipRole.MEMBERSHIP_ROLE_ORG_CONTRIBUTOR; case -1: case "UNRECOGNIZED": default: @@ -113,6 +117,8 @@ export function membershipRoleToJSON(object: MembershipRole): string { return "MEMBERSHIP_ROLE_ORG_OWNER"; case MembershipRole.MEMBERSHIP_ROLE_ORG_MEMBER: return "MEMBERSHIP_ROLE_ORG_MEMBER"; + case MembershipRole.MEMBERSHIP_ROLE_ORG_CONTRIBUTOR: + return "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR"; case MembershipRole.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json index 667d4a813..822523322 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json @@ -31,7 +31,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json index 686d42376..c0670f8fa 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json @@ -31,7 +31,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json index 44e076d7e..47c1842b3 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json @@ -29,7 +29,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json index 4dbed371a..c17d3cfde 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json @@ -29,7 +29,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json index 10af855d4..7d05ad2c6 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json @@ -31,7 +31,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json index dffa4d149..d6a6b3561 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json @@ -31,7 +31,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json index 251e3f0bf..a9be17c28 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json @@ -33,7 +33,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json index 85d0689d8..2eba77746 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json @@ -33,7 +33,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json index 4777c055d..f31133acf 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json @@ -21,7 +21,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json index 6c30732d5..b8a0554eb 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json @@ -21,7 +21,8 @@ "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", "MEMBERSHIP_ROLE_ORG_OWNER", - "MEMBERSHIP_ROLE_ORG_MEMBER" + "MEMBERSHIP_ROLE_ORG_MEMBER", + "MEMBERSHIP_ROLE_ORG_CONTRIBUTOR" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 50c9e9f9d..f9bdc41e5 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -126,6 +126,12 @@ func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.Attesta return nil, errors.NotFound("not found", "contract not found") } + // If contract is private, check we have permissions on the project + scopedEntity := contractVersion.Contract.ScopedEntity + if rbacEnabled(ctx) && contractVersion.Contract.IsProjectScoped() && scopedEntity.ID != wf.ProjectID { + return nil, errors.NotFound("not found", "contract not found") + } + resp := &cpAPI.AttestationServiceGetContractResponse_Result{ Workflow: bizWorkflowToPb(wf), Contract: bizWorkFlowContractVersionToPb(contractVersion.Version), @@ -674,10 +680,14 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP // try to load project and apply RBAC if needed if _, err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, &cpAPI.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil { - // if the project is not found, we ignore the error, since we'll create the project in this call + // if the project is not found, check if user can create projects if !errors.IsNotFound(err) { return nil, err } + + if err = s.userCanCreateProject(ctx); err != nil { + return nil, err + } } // we need this token to forward it to the provider service next diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index 640155827..f98ba77fc 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -128,7 +128,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS if mapping != nil { backend = mapping.CASBackend - } else if currentAuthzSubject == string(authz.RoleAdmin) || currentAuthzSubject == string(authz.RoleOwner) { + } else if authz.Role(currentAuthzSubject).IsAdmin() { // fallback to default mapping for admins backend = defaultBackend } diff --git a/app/controlplane/internal/service/group.go b/app/controlplane/internal/service/group.go index b9b612efd..02a78c35b 100644 --- a/app/controlplane/internal/service/group.go +++ b/app/controlplane/internal/service/group.go @@ -265,6 +265,10 @@ func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceList RequesterID: requesterUUID, } + if err = g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, currentOrg.ID, req.GetGroupReference(), authz.PolicyGroupListMemberships); err != nil { + return nil, err + } + // Parse groupID and groupName from the request opts.ID, opts.Name, err = req.GetGroupReference().Parse() if err != nil { @@ -509,6 +513,7 @@ func (g *GroupService) ListProjects(ctx context.Context, req *pb.GroupServiceLis orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { return nil, errors.BadRequest("invalid", "invalid organization ID") + } // Parse groupID and groupName from the request @@ -585,7 +590,7 @@ func (g *GroupService) userHasPermissionOnGroupMembershipsWithPolicy(ctx context } // Allow if user has admin or owner role - if userRole == string(authz.RoleAdmin) || userRole == string(authz.RoleOwner) { + if authz.Role(userRole).IsAdmin() { return nil } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 63f794428..b422265f4 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -253,6 +253,32 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, return p, nil } +func (s *service) userCanCreateProject(ctx context.Context) error { + // admins always can create projects + if !rbacEnabled(ctx) { + return nil + } + + // Only org tokens can create projects + if token := entities.CurrentAPIToken(ctx); token != nil { + if token.ProjectID != nil { + return errors.Forbidden("unauthorized", "you are not allowed to create projects") + } + } + + orgRole := usercontext.CurrentAuthzSubject(ctx) + pass, err := s.enforcer.Enforce(orgRole, authz.PolicyProjectCreate) + if err != nil { + return handleUseCaseErr(err, s.log) + } + + if !pass { + return errors.Forbidden("unauthorized", "you are not allowed to create projects") + } + + return nil +} + // visibleProjects returns projects where the user has any role (currently ProjectAdmin and ProjectViewer) func (s *service) visibleProjects(ctx context.Context) []uuid.UUID { if !rbacEnabled(ctx) { @@ -312,7 +338,7 @@ func rbacEnabled(ctx context.Context) bool { // we have an user currentSubject := usercontext.CurrentAuthzSubject(ctx) - return currentSubject == string(authz.RoleOrgMember) + return authz.Role(currentSubject).RBACEnabled() } // NOTE: some of these http errors get automatically translated to gRPC status codes diff --git a/app/controlplane/internal/service/user.go b/app/controlplane/internal/service/user.go index ae12dc08e..60bfc00c8 100644 --- a/app/controlplane/internal/service/user.go +++ b/app/controlplane/internal/service/user.go @@ -124,6 +124,8 @@ func bizRoleToPb(r authz.Role) pb.MembershipRole { return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER case authz.RoleOrgMember: return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER + case authz.RoleOrgContributor: + return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_CONTRIBUTOR default: return pb.MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index c44625d6d..4c918f1ba 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -60,10 +60,14 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre } if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil { - // if the project is not found, we ignore the error, since we'll create the project in this call + // if the project is not found, check if user can create projects if !errors.IsNotFound(err) { return nil, err } + + if err = s.userCanCreateProject(ctx); err != nil { + return nil, err + } } createOpts := &biz.WorkflowCreateOpts{ diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index a4942588c..f243f0f20 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -129,7 +129,7 @@ func WithAttestationContextFromUser(userUC *biz.UserUseCase, membershipUC *biz.M // TODO: move to authz middleware once we add support for all the tokens // for now in that middleware we are not mapping admins nor owners to a specific role // Admins and Owners can perform any operation. Members will need additional RBAC behaviour at service layer - if subject != string(authz.RoleAdmin) && subject != string(authz.RoleOwner) && subject != string(authz.RoleOrgMember) { + if !authz.Role(subject).IsAdmin() && !authz.Role(subject).RBACEnabled() { return nil, fmt.Errorf("your user doesn't have permissions to perform attestations in this organization, role=%s, orgID=%s", subject, org.ID) } diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 7bc141618..16682b9b5 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -24,6 +24,15 @@ type Policy struct { type Role string +// RBACEnabled returns whether an org-scoped role has RBAC enabled and needs resource-scoped enforcement. +func (r Role) RBACEnabled() bool { + return r == RoleOrgMember || r == RoleOrgContributor +} + +func (r Role) IsAdmin() bool { + return r == RoleAdmin || r == RoleOwner +} + const ( // Actions @@ -46,6 +55,7 @@ const ( ResourceRobotAccount = "robot_account" ResourceWorkflowRun = "workflow_run" ResourceWorkflow = "workflow" + ResourceProject = "project" Organization = "organization" OrganizationMemberships = "organization_memberships" ResourceGroup = "group" @@ -67,12 +77,14 @@ const ( // New RBAC roles - // RoleOrgMember is the role that users get by default when they join an organization. - // They cannot see projects until they are invited. However, they are able to create their own projects, + // RoleOrgMember cannot see projects until they are invited. However, they are able to create their own projects, // so Casbin rules (role, resource-type, action) are NOT enough to check for permission, since we must check for ownership as well. // That last check will be done at the service level. RoleOrgMember Role = "role:org:member" + // RoleOrgContributor can work on projects they are invited to with scoped role ProjectAdmin or ProjectViewer, but they cannot create their own projects. + RoleOrgContributor Role = "role:org:contributor" + RoleProjectAdmin Role = "role:project:admin" RoleProjectViewer Role = "role:project:viewer" @@ -93,6 +105,7 @@ var ManagedResources = []string{ ResourceRobotAccount, ResourceWorkflowRun, ResourceWorkflow, + ResourceProject, Organization, OrganizationMemberships, ResourceGroup, @@ -143,20 +156,18 @@ var ( PolicyWorkflowCreate = &Policy{ResourceWorkflow, ActionCreate} PolicyWorkflowUpdate = &Policy{ResourceWorkflow, ActionUpdate} PolicyWorkflowDelete = &Policy{ResourceWorkflow, ActionDelete} + // Projects + PolicyProjectCreate = &Policy{ResourceProject, ActionCreate} // User Membership - PolicyOrganizationRead = &Policy{Organization, ActionRead} - PolicyOrganizationListMemberships = &Policy{OrganizationMemberships, ActionList} - // Groups - PolicyGroupList = &Policy{ResourceGroup, ActionList} - PolicyGroupListPendingInvitations = &Policy{ResourceGroup, ActionList} - PolicyGroupRead = &Policy{ResourceGroup, ActionRead} + PolicyOrganizationRead = &Policy{Organization, ActionRead} + // Group Memberships - PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} - PolicyGroupAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} - PolicyGroupRemoveMemberships = &Policy{ResourceGroupMembership, ActionDelete} - PolicyGroupUpdateMemberships = &Policy{ResourceGroupMembership, ActionUpdate} - // Group-Projects - PolicyGroupListProjects = &Policy{ResourceGroupProjects, ActionList} + PolicyGroupListPendingInvitations = &Policy{ResourceGroup, ActionList} + PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} + PolicyGroupAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} + PolicyGroupRemoveMemberships = &Policy{ResourceGroupMembership, ActionDelete} + PolicyGroupUpdateMemberships = &Policy{ResourceGroupMembership, ActionUpdate} + // API Token PolicyAPITokenList = &Policy{ResourceAPIToken, ActionList} PolicyAPITokenCreate = &Policy{ResourceAPIToken, ActionCreate} @@ -214,9 +225,10 @@ var RolesMap = map[Role][]*Policy{ PolicyOrganizationInvitationsCreate, // + all the policies from the viewer role inherited automatically }, + // RoleOrgMember is an org-scoped role that enables RBAC in the underlying resources. Users with this role at // the organization level will need specific project roles to access their contents - RoleOrgMember: { + RoleOrgContributor: { // Allowed endpoints. RBAC will be applied where needed PolicyWorkflowRead, PolicyWorkflowContractList, @@ -253,30 +265,23 @@ var RolesMap = map[Role][]*Policy{ PolicyOrgMetricsRead, PolicyReferrerRead, - // Groups - PolicyGroupList, - PolicyGroupRead, - - // Group Projects - PolicyGroupListProjects, - - // Group Memberships - PolicyGroupListMemberships, - // Additional check for API tokens is done at the service level PolicyAPITokenList, PolicyAPITokenCreate, PolicyAPITokenRevoke, - // Project Memberships + // Project Memberships available to contributors if they are project admins PolicyProjectListMemberships, PolicyProjectAddMemberships, PolicyProjectRemoveMemberships, PolicyProjectUpdateMemberships, + }, - // Org memberships - PolicyOrganizationListMemberships, + // RoleOrgMember inherits from RoleOrgContributor and can also create their own projects + RoleOrgMember: { + PolicyProjectCreate, }, + // RoleProjectViewer: has read-only permissions on a project RoleProjectViewer: { PolicyWorkflowRead, @@ -287,18 +292,14 @@ var RolesMap = map[Role][]*Policy{ // Project API Token PolicyAPITokenList, }, - // RoleProjectAdmin: represents a project administrator. It's the higher role in project resources, - // and it's only considered when the org-level role is `RoleOrgMember` + // RoleProjectAdmin: inherits from ProjectViewer and represents a project administrator. RoleProjectAdmin: { // workflow contracts - PolicyWorkflowContractList, - PolicyWorkflowContractRead, PolicyWorkflowContractCreate, PolicyWorkflowContractUpdate, PolicyWorkflowContractDelete, // attestations - PolicyWorkflowRead, PolicyWorkflowCreate, PolicyWorkflowRunCreate, PolicyWorkflowRunUpdate, // to reset attestations @@ -307,15 +308,11 @@ var RolesMap = map[Role][]*Policy{ PolicyWorkflowUpdate, PolicyWorkflowDelete, - // workflow runs - PolicyWorkflowRunRead, - // integrations PolicyAttachedIntegrationAttach, PolicyAttachedIntegrationDetach, // Project API Token - PolicyAPITokenList, PolicyAPITokenCreate, PolicyAPITokenRevoke, @@ -327,8 +324,9 @@ var RolesMap = map[Role][]*Policy{ }, // RoleGroupMaintainer: represents a group maintainer role. RoleGroupMaintainer: { - PolicyGroupListPendingInvitations, + // Group Memberships PolicyGroupListMemberships, + PolicyGroupListPendingInvitations, PolicyGroupAddMemberships, PolicyGroupRemoveMemberships, PolicyGroupUpdateMemberships, @@ -395,28 +393,26 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.UserService/DeleteMembership": {}, "/controlplane.v1.AuthService/DeleteAccount": {}, - "/controlplane.v1.OrganizationService/ListMemberships": {PolicyOrganizationListMemberships}, - // Groups - "/controlplane.v1.GroupService/List": {PolicyGroupList}, - "/controlplane.v1.GroupService/Get": {PolicyGroupRead}, - // Group Memberships - "/controlplane.v1.GroupService/ListProjects": {PolicyGroupListProjects}, - // For the following endpoints, we rely on the service layer to check the permissions - // That's why we let everyone access them (empty policies) + // Groups (everyone see groups) + "/controlplane.v1.GroupService/List": {}, + "/controlplane.v1.GroupService/Get": {}, + + // For the following endpoints, we rely on the service layer to check the permissions. + // That's why we let everyone access them (empty policies). + // Group Memberships are only available to org admins or maintainers "/controlplane.v1.GroupService/ListMembers": {}, + "/controlplane.v1.GroupService/ListProjects": {}, "/controlplane.v1.GroupService/AddMember": {}, "/controlplane.v1.GroupService/RemoveMember": {}, "/controlplane.v1.GroupService/ListPendingInvitations": {}, "/controlplane.v1.GroupService/UpdateMemberMaintainerStatus": {}, - // Projects: Check happen at service level - // Project Memberships - "/controlplane.v1.ProjectService/ListMembers": {}, - "/controlplane.v1.ProjectService/AddMember": {}, - "/controlplane.v1.ProjectService/RemoveMember": {}, - "/controlplane.v1.ProjectService/UpdateMemberRole": {}, - "/controlplane.v1.ProjectService/ListPendingInvitations": {}, + "/controlplane.v1.ProjectService/ListMembers": {PolicyProjectListMemberships}, + "/controlplane.v1.ProjectService/AddMember": {PolicyProjectAddMemberships}, + "/controlplane.v1.ProjectService/RemoveMember": {PolicyProjectRemoveMemberships}, + "/controlplane.v1.ProjectService/UpdateMemberRole": {PolicyProjectUpdateMemberships}, + "/controlplane.v1.ProjectService/ListPendingInvitations": {PolicyProjectListMemberships}, // API tokens RBAC are handled at the service level "/controlplane.v1.APITokenService/List": {PolicyAPITokenList}, @@ -434,6 +430,7 @@ func (Role) Values() (roles []string) { // RBAC roles RoleOrgMember, + RoleOrgContributor, RoleProjectAdmin, RoleProjectViewer, RoleGroupMaintainer, diff --git a/app/controlplane/pkg/authz/enforcer.go b/app/controlplane/pkg/authz/enforcer.go index 2301c90fe..ddc5a8fee 100644 --- a/app/controlplane/pkg/authz/enforcer.go +++ b/app/controlplane/pkg/authz/enforcer.go @@ -258,5 +258,17 @@ func doSync(e *Enforcer, c *Config) error { return fmt.Errorf("failed to add grouping policy: %w", err) } + // Members are contributors as well + _, err = e.AddGroupingPolicy(string(RoleOrgMember), string(RoleOrgContributor)) + if err != nil { + return fmt.Errorf("failed to add grouping policy: %w", err) + } + + // ProjectAdmins are ProjectViewers as well + _, err = e.AddGroupingPolicy(string(RoleProjectAdmin), string(RoleProjectViewer)) + if err != nil { + return fmt.Errorf("failed to add grouping policy: %w", err) + } + return nil } diff --git a/app/controlplane/pkg/authz/middleware/middleware.go b/app/controlplane/pkg/authz/middleware/middleware.go index d90514ce9..10b17637b 100644 --- a/app/controlplane/pkg/authz/middleware/middleware.go +++ b/app/controlplane/pkg/authz/middleware/middleware.go @@ -57,7 +57,7 @@ func WithAuthzMiddleware(enforcer Enforcer, logger *log.Helper) middleware.Middl // We do not have all the policies related to an admin user defined yet // so for now we skip the authorization check for admin users since they are allowed to do anything // TODO: fill out the rest of the policies in authz.ServerOperationsMap and remove this check - if subject == string(authz.RoleAdmin) || subject == string(authz.RoleOwner) { + if authz.Role(subject).IsAdmin() { logger.Infow("msg", "[authZ] skipped", "sub", subject, "operation", apiOperation) return handler(ctx, req) } diff --git a/app/controlplane/pkg/biz/group.go b/app/controlplane/pkg/biz/group.go index 77bdb63c9..1272b7d7e 100644 --- a/app/controlplane/pkg/biz/group.go +++ b/app/controlplane/pkg/biz/group.go @@ -509,8 +509,7 @@ func (uc *GroupUseCase) validateRequesterPermissions(ctx context.Context, orgID, } // Allow if the requester is an org owner or admin - isAdminOrOwner := requesterMembership.Role == authz.RoleOwner || requesterMembership.Role == authz.RoleAdmin - if isAdminOrOwner { + if requesterMembership.Role.IsAdmin() { return nil } @@ -563,7 +562,7 @@ func (uc *GroupUseCase) handleNonExistingUser(ctx context.Context, orgID, groupI } // Create an invitation for the user to join the organization - if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.RequesterID.String(), opts.UserEmail, WithInvitationRole(authz.RoleOrgMember), WithInvitationContext(invitationContext)); err != nil { + if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.RequesterID.String(), opts.UserEmail, WithInvitationRole(authz.RoleOrgContributor), WithInvitationContext(invitationContext)); err != nil { return nil, fmt.Errorf("failed to create invitation: %w", err) } @@ -726,10 +725,8 @@ func (uc *GroupUseCase) UpdateMemberMaintainerStatus(ctx context.Context, orgID // Check if the requester has sufficient permissions // Allow if the requester is an org owner or admin - isAdminOrOwner := requesterMembership.Role == authz.RoleOwner || requesterMembership.Role == authz.RoleAdmin - // If not an admin/owner, check if the requester is a maintainer of this group - if !isAdminOrOwner { + if !requesterMembership.Role.IsAdmin() { // Check if the requester is a maintainer of this group requesterGroupMembership, err := uc.membershipRepo.FindByUserAndResourceID(ctx, opts.RequesterID, resolvedGroupID) if err != nil && !IsNotFound(err) { diff --git a/app/controlplane/pkg/biz/group_integration_test.go b/app/controlplane/pkg/biz/group_integration_test.go index 28d54f7d7..68c7388c2 100644 --- a/app/controlplane/pkg/biz/group_integration_test.go +++ b/app/controlplane/pkg/biz/group_integration_test.go @@ -1017,7 +1017,7 @@ func (s *groupMembersIntegrationTestSuite) TestAddMemberToGroup() { if inv.ReceiverEmail == "not-in-org@example.com" { found = true s.Equal(biz.OrgInvitationStatusPending, inv.Status) - s.Equal(string(authz.RoleOrgMember), string(inv.Role)) + s.Equal(string(authz.RoleOrgContributor), string(inv.Role)) // Verify the invitation context s.NotNil(inv.Context, "Invitation context should not be nil") diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 3a4fa5531..5fc55cbb1 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -401,7 +401,7 @@ func (uc *MembershipUseCase) GetOrgsAndRBACInfoForUser(ctx context.Context, user if m.ResourceType == authz.ResourceTypeOrganization { userOrgs = append(userOrgs, m.ResourceID) // If the role in the org is member, we must enable RBAC for projects. - if m.Role == authz.RoleOrgMember { + if m.Role.RBACEnabled() { // get the list of projects in org, and match it with the memberships to build a filter. // note that appending an empty slice to a nil slice doesn't change it (it's still nil) projectIDs[m.ResourceID] = getProjectsWithMembershipInOrg(m.ResourceID, memberships) diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index 508d6d3df..3500e67fb 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -642,7 +642,7 @@ func (uc *ProjectUseCase) verifyRequesterHasPermissions(ctx context.Context, org } // Second check: if requester is an org owner or admin, they have all permissions - if currentOrgMembership.Role == authz.RoleOwner || currentOrgMembership.Role == authz.RoleAdmin { + if currentOrgMembership.Role.IsAdmin() { return nil } diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index 3d21bc3fa..883122506 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -248,6 +248,8 @@ func PbRoleToBiz(r pb.MembershipRole) authz.Role { return authz.RoleViewer case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER: return authz.RoleOrgMember + case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_CONTRIBUTOR: + return authz.RoleOrgContributor default: return "" } diff --git a/app/controlplane/pkg/data/ent/membership/membership.go b/app/controlplane/pkg/data/ent/membership/membership.go index db3c87067..30a4b25f9 100644 --- a/app/controlplane/pkg/data/ent/membership/membership.go +++ b/app/controlplane/pkg/data/ent/membership/membership.go @@ -104,7 +104,7 @@ var ( // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer": return nil default: return fmt.Errorf("membership: invalid enum value for role field: %q", r) diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index fa48502c0..66e91d0d3 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -320,7 +320,7 @@ var ( {Name: "current", Type: field.TypeBool, Default: false}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "updated_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, - {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, + {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, {Name: "membership_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"user", "group"}}, {Name: "member_id", Type: field.TypeUUID, Nullable: true}, {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project", "group"}}, @@ -367,7 +367,7 @@ var ( {Name: "status", Type: field.TypeEnum, Enums: []string{"accepted", "pending"}, Default: "pending"}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, - {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, + {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, {Name: "context", Type: field.TypeJSON, Nullable: true}, {Name: "organization_id", Type: field.TypeUUID}, {Name: "sender_id", Type: field.TypeUUID}, diff --git a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go index cc6a3ed5b..f5c5e4f9c 100644 --- a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go +++ b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go @@ -101,7 +101,7 @@ func StatusValidator(s biz.OrgInvitationStatus) error { // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer": return nil default: return fmt.Errorf("orginvitation: invalid enum value for role field: %q", r)