package service import ( "context" "fmt" "strconv" "time" "github.com/RafaySystems/rcloud-base/internal/dao" "github.com/RafaySystems/rcloud-base/internal/models" "github.com/RafaySystems/rcloud-base/internal/persistence/provider/pg" authzv1 "github.com/RafaySystems/rcloud-base/proto/types/authz" v3 "github.com/RafaySystems/rcloud-base/proto/types/commonpb/v3" userv3 "github.com/RafaySystems/rcloud-base/proto/types/userpb/v3" "github.com/google/uuid" bun "github.com/uptrace/bun" "google.golang.org/protobuf/types/known/timestamppb" ) const ( groupKind = "Group" groupListKind = "GroupList" ) // GroupService is the interface for group operations type GroupService interface { // create group Create(context.Context, *userv3.Group) (*userv3.Group, error) // get group by id GetByID(context.Context, *userv3.Group) (*userv3.Group, error) // get group by name GetByName(context.Context, *userv3.Group) (*userv3.Group, error) // create or update group Update(context.Context, *userv3.Group) (*userv3.Group, error) // delete group Delete(context.Context, *userv3.Group) (*userv3.Group, error) // list groups List(context.Context, *userv3.Group) (*userv3.GroupList, error) } // groupService implements GroupService type groupService struct { db *bun.DB azc AuthzService } // NewGroupService return new group service func NewGroupService(db *bun.DB, azc AuthzService) GroupService { return &groupService{db: db, azc: azc} } func (s *groupService) deleteGroupRoleRelaitons(ctx context.Context, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { // delete previous entries // TODO: single delete command err := pg.DeleteX(ctx, s.db, "group_id", groupId, &models.GroupRole{}) if err != nil { return &userv3.Group{}, err } err = pg.DeleteX(ctx, s.db, "group_id", groupId, &models.ProjectGroupRole{}) if err != nil { return &userv3.Group{}, err } err = pg.DeleteX(ctx, s.db, "group_id", groupId, &models.ProjectGroupNamespaceRole{}) if err != nil { return &userv3.Group{}, err } _, err = s.azc.DeletePolicies(ctx, &authzv1.Policy{Sub: "g:" + group.GetMetadata().GetName()}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to delete group-role relations from authz; %v", err) } return group, nil } // Map roles to groups func (s *groupService) createGroupRoleRelations(ctx context.Context, group *userv3.Group, ids parsedIds) (*userv3.Group, error) { // TODO: add transactions projectNamespaceRoles := group.GetSpec().GetProjectNamespaceRoles() var pgnrs []models.ProjectGroupNamespaceRole var pgrs []models.ProjectGroupRole var grs []models.GroupRole var ps []*authzv1.Policy for _, pnr := range projectNamespaceRoles { role := pnr.GetRole() entity, err := pg.GetIdByName(ctx, s.db, role, &models.Role{}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role) } var roleId uuid.UUID if rle, ok := entity.(*models.Role); ok { roleId = rle.ID } else { return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role) } project := pnr.GetProject() org := group.GetMetadata().GetOrganization() namespaceId := pnr.GetNamespace() // TODO: lookup id from name switch { case namespaceId != 0: projectId, err := pg.GetProjectId(ctx, s.db, project) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project) } pgnr := models.ProjectGroupNamespaceRole{ Trash: false, RoleId: roleId, PartnerId: ids.Partner, OrganizationId: ids.Organization, GroupId: ids.Id, ProjectId: projectId, NamespaceId: namespaceId, Active: true, } pgnrs = append(pgnrs, pgnr) ps = append(ps, &authzv1.Policy{ Sub: "g:" + group.GetMetadata().GetName(), Ns: strconv.FormatInt(namespaceId, 10), Proj: project, Org: org, Obj: role, Act: "*", }) case project != "": projectId, err := pg.GetProjectId(ctx, s.db, project) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project) } pgr := models.ProjectGroupRole{ Trash: false, RoleId: roleId, PartnerId: ids.Partner, OrganizationId: ids.Organization, GroupId: ids.Id, ProjectId: projectId, Active: true, } pgrs = append(pgrs, pgr) ps = append(ps, &authzv1.Policy{ Sub: "g:" + group.GetMetadata().GetName(), Ns: "*", Proj: project, Org: org, Obj: role, Act: "*", }) default: gr := models.GroupRole{ Trash: false, RoleId: roleId, PartnerId: ids.Partner, OrganizationId: ids.Organization, GroupId: ids.Id, Active: true, } grs = append(grs, gr) ps = append(ps, &authzv1.Policy{ Sub: "g:" + group.GetMetadata().GetName(), Ns: "*", Proj: "*", Org: org, Obj: role, Act: "*", }) } } if len(pgnrs) > 0 { _, err := pg.Create(ctx, s.db, &pgnrs) if err != nil { return &userv3.Group{}, err } } if len(pgrs) > 0 { _, err := pg.Create(ctx, s.db, &pgrs) if err != nil { return &userv3.Group{}, err } } if len(grs) > 0 { _, err := pg.Create(ctx, s.db, &grs) if err != nil { return &userv3.Group{}, err } } if len(ps) > 0 { success, err := s.azc.CreatePolicies(ctx, &authzv1.Policies{Policies: ps}) if err != nil || !success.Res { return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err) } } return group, nil } func (s *groupService) deleteGroupAccountRelations(ctx context.Context, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { err := pg.DeleteX(ctx, s.db, "group_id", groupId, &models.GroupAccount{}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to delete user; %v", err) } _, err = s.azc.DeleteUserGroups(ctx, &authzv1.UserGroup{Grp: "g:" + group.GetMetadata().GetName()}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to delete group-user relations from authz; %v", err) } return group, nil } // Update the users(account) mapped to each group func (s *groupService) createGroupAccountRelations(ctx context.Context, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { // TODO: add transactions var grpaccs []models.GroupAccount var ugs []*authzv1.UserGroup for _, account := range unique(group.GetSpec().GetUsers()) { // FIXME: do combined lookup entity, err := pg.GetIdByTraits(ctx, s.db, account, &models.KratosIdentities{}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find user '%v'", account) } if acc, ok := entity.(*models.KratosIdentities); ok { grp := models.GroupAccount{ CreatedAt: time.Now(), ModifiedAt: time.Now(), Trash: false, AccountId: acc.ID, GroupId: groupId, Active: true, } grpaccs = append(grpaccs, grp) ugs = append(ugs, &authzv1.UserGroup{ Grp: "g:" + group.GetMetadata().GetName(), User: "u:" + account, }) } } if len(grpaccs) == 0 { return group, nil } _, err := pg.Create(ctx, s.db, &grpaccs) if err != nil { return &userv3.Group{}, err } // TODO: revert our db inserts if this fails // Just FYI, the success can be false if we delete the db directly but casbin has it available internally _, err = s.azc.CreateUserGroups(ctx, &authzv1.UserGroups{UserGroups: ugs}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err) } return group, nil } func (s *groupService) getPartnerOrganization(ctx context.Context, group *userv3.Group) (uuid.UUID, uuid.UUID, error) { partner := group.GetMetadata().GetPartner() org := group.GetMetadata().GetOrganization() partnerId, err := pg.GetPartnerId(ctx, s.db, partner) if err != nil { return uuid.Nil, uuid.Nil, err } organizationId, err := pg.GetOrganizationId(ctx, s.db, org) if err != nil { return partnerId, uuid.Nil, err } return partnerId, organizationId, nil } func (s *groupService) Create(ctx context.Context, group *userv3.Group) (*userv3.Group, error) { partnerId, organizationId, err := s.getPartnerOrganization(ctx, group) if err != nil { return nil, fmt.Errorf("unable to get partner and org id") } g, _ := pg.GetIdByNamePartnerOrg(ctx, s.db, group.GetMetadata().GetName(), uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{}) if g != nil { return nil, fmt.Errorf("group '%v' already exists", group.GetMetadata().GetName()) } //convert v3 spec to internal models grp := models.Group{ Name: group.GetMetadata().GetName(), Description: group.GetMetadata().GetDescription(), CreatedAt: time.Now(), ModifiedAt: time.Now(), Trash: false, OrganizationId: organizationId, PartnerId: partnerId, Type: group.GetSpec().GetType(), } entity, err := pg.Create(ctx, s.db, &grp) if err != nil { return &userv3.Group{}, err } //update v3 spec if grp, ok := entity.(*models.Group); ok { // we can get previous group using the id, find users/roles from that and delete those group, err = s.createGroupAccountRelations(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } group, err = s.createGroupRoleRelations(ctx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) if err != nil { return &userv3.Group{}, err } return group, nil } return &userv3.Group{}, fmt.Errorf("unable to create group") } func (s *groupService) toV3Group(ctx context.Context, group *userv3.Group, grp *models.Group) (*userv3.Group, error) { labels := make(map[string]string) labels["organization"] = group.GetMetadata().GetOrganization() labels["partner"] = group.GetMetadata().GetPartner() group.ApiVersion = apiVersion group.Kind = groupKind group.Metadata = &v3.Metadata{ Name: grp.Name, Description: grp.Description, Organization: group.GetMetadata().GetOrganization(), Partner: group.GetMetadata().GetPartner(), Labels: labels, ModifiedAt: timestamppb.New(grp.ModifiedAt), } users, err := dao.GetUsers(ctx, s.db, grp.ID) if err != nil { return &userv3.Group{}, err } userNames := []string{} for _, u := range users { userNames = append(userNames, u.Traits["email"].(string)) } roles, err := dao.GetGroupRoles(ctx, s.db, grp.ID) if err != nil { return &userv3.Group{}, err } group.Spec = &userv3.GroupSpec{ Type: grp.Type, Users: userNames, ProjectNamespaceRoles: roles, } return group, nil } func (s *groupService) GetByID(ctx context.Context, group *userv3.Group) (*userv3.Group, error) { id := group.GetMetadata().GetId() uid, err := uuid.Parse(id) if err != nil { return &userv3.Group{}, err } entity, err := pg.GetByID(ctx, s.db, uid, &models.Group{}) if err != nil { return &userv3.Group{}, err } if grp, ok := entity.(*models.Group); ok { return s.toV3Group(ctx, group, grp) } return group, nil } func (s *groupService) GetByName(ctx context.Context, group *userv3.Group) (*userv3.Group, error) { name := group.GetMetadata().GetName() partnerId, organizationId, err := s.getPartnerOrganization(ctx, group) if err != nil { return nil, fmt.Errorf("unable to get partner and org id") } entity, err := pg.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{}) if err != nil { return &userv3.Group{}, err } if grp, ok := entity.(*models.Group); ok { return s.toV3Group(ctx, group, grp) } return group, nil } func (s *groupService) Update(ctx context.Context, group *userv3.Group) (*userv3.Group, error) { // TODO: inform when unchanged name := group.GetMetadata().GetName() partnerId, organizationId, err := s.getPartnerOrganization(ctx, group) if err != nil { return nil, fmt.Errorf("unable to get partner and org id") } entity, err := pg.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{}) if err != nil { return &userv3.Group{}, fmt.Errorf("no group found with name '%v'", name) } if grp, ok := entity.(*models.Group); ok { // TODO: are we not letting them update org/partner? grp.Name = group.Metadata.Name grp.Description = group.Metadata.Description grp.Type = group.Spec.Type grp.ModifiedAt = time.Now() // update account/role links group, err = s.deleteGroupAccountRelations(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } group, err = s.createGroupAccountRelations(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } group, err = s.deleteGroupRoleRelaitons(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } group, err = s.createGroupRoleRelations(ctx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) if err != nil { return &userv3.Group{}, err } _, err = pg.Update(ctx, s.db, grp.ID, grp) if err != nil { return &userv3.Group{}, err } // update spec and status group.Spec = &userv3.GroupSpec{ Type: grp.Type, Users: group.Spec.Users, // TODO: update from db resp or no update? ProjectNamespaceRoles: group.Spec.ProjectNamespaceRoles, } } return group, nil } func (s *groupService) Delete(ctx context.Context, group *userv3.Group) (*userv3.Group, error) { name := group.GetMetadata().GetName() partnerId, organizationId, err := s.getPartnerOrganization(ctx, group) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to get partner and org id") } entity, err := pg.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{}) if err != nil { return &userv3.Group{}, err } if grp, ok := entity.(*models.Group); ok { group, err = s.deleteGroupRoleRelaitons(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } group, err = s.deleteGroupAccountRelations(ctx, grp.ID, group) if err != nil { return &userv3.Group{}, err } err = pg.Delete(ctx, s.db, grp.ID, grp) if err != nil { return &userv3.Group{}, err } } return group, nil } func (s *groupService) List(ctx context.Context, group *userv3.Group) (*userv3.GroupList, error) { var groups []*userv3.Group groupList := &userv3.GroupList{ ApiVersion: apiVersion, Kind: groupListKind, Metadata: &v3.ListMetadata{ Count: 0, }, } if len(group.Metadata.Organization) > 0 { orgId, err := pg.GetOrganizationId(ctx, s.db, group.Metadata.Organization) if err != nil { return groupList, err } partId, err := pg.GetPartnerId(ctx, s.db, group.Metadata.Partner) if err != nil { return groupList, err } var grps []models.Group entities, err := pg.List(ctx, s.db, uuid.NullUUID{UUID: partId, Valid: true}, uuid.NullUUID{UUID: orgId, Valid: true}, &grps) if err != nil { return groupList, err } if grps, ok := entities.(*[]models.Group); ok { for _, grp := range *grps { entry := &userv3.Group{Metadata: group.GetMetadata()} entry, err = s.toV3Group(ctx, entry, &grp) if err != nil { return groupList, err } groups = append(groups, entry) } //update the list metadata and items response groupList.Metadata = &v3.ListMetadata{ Count: int64(len(groups)), } groupList.Items = groups } } else { return groupList, fmt.Errorf("missing organization id in metadata") } return groupList, nil }