mirror of
https://github.com/paralus/paralus.git
synced 2026-05-07 00:46:52 +00:00
This was done inorder to support transactions which will be done in the next PR. This is the first step towards that.
536 lines
15 KiB
Go
536 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
bun "github.com/uptrace/bun"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/RafaySystems/rcloud-base/internal/dao"
|
|
"github.com/RafaySystems/rcloud-base/internal/models"
|
|
providers "github.com/RafaySystems/rcloud-base/internal/persistence/provider/kratos"
|
|
"github.com/RafaySystems/rcloud-base/internal/persistence/provider/pg"
|
|
"github.com/RafaySystems/rcloud-base/pkg/common"
|
|
userrpcv3 "github.com/RafaySystems/rcloud-base/proto/rpc/user"
|
|
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"
|
|
)
|
|
|
|
const (
|
|
userKind = "User"
|
|
userListKind = "UserList"
|
|
)
|
|
|
|
// GroupService is the interface for group operations
|
|
type UserService interface {
|
|
// create user
|
|
Create(context.Context, *userv3.User) (*userv3.User, error)
|
|
// get user by id
|
|
GetByID(context.Context, *userv3.User) (*userv3.User, error)
|
|
// // get user by name
|
|
GetByName(context.Context, *userv3.User) (*userv3.User, error)
|
|
// create or update user
|
|
Update(context.Context, *userv3.User) (*userv3.User, error)
|
|
// delete user
|
|
Delete(context.Context, *userv3.User) (*userrpcv3.DeleteUserResponse, error)
|
|
// list users
|
|
List(context.Context, *userv3.User) (*userv3.UserList, error)
|
|
// retrieve the cli config for the logged in user
|
|
RetrieveCliConfig(ctx context.Context, req *userrpcv3.ApiKeyRequest) (*common.CliConfigDownloadData, error)
|
|
}
|
|
|
|
type userService struct {
|
|
ap providers.AuthProvider
|
|
db *bun.DB
|
|
azc AuthzService
|
|
ks ApiKeyService
|
|
cc common.CliConfigDownloadData
|
|
}
|
|
|
|
type userTraits struct {
|
|
Email string
|
|
FirstName string
|
|
LastName string
|
|
Description string
|
|
}
|
|
|
|
// FIXME: find a better way to do this
|
|
type parsedIds struct {
|
|
Id uuid.UUID
|
|
Partner uuid.UUID
|
|
Organization uuid.UUID
|
|
}
|
|
|
|
func NewUserService(ap providers.AuthProvider, db *bun.DB, azc AuthzService, kss ApiKeyService, cfg common.CliConfigDownloadData) UserService {
|
|
return &userService{ap: ap, db: db, azc: azc, ks: kss, cc: cfg}
|
|
}
|
|
|
|
func getUserTraits(traits map[string]interface{}) userTraits {
|
|
// FIXME: is there a better way to do this?
|
|
// All of these should ideally be available as we have the identities schema, but just in case
|
|
email, ok := traits["email"]
|
|
if !ok {
|
|
email = ""
|
|
}
|
|
fname, ok := traits["first_name"]
|
|
if !ok {
|
|
fname = ""
|
|
}
|
|
lname, ok := traits["last_name"]
|
|
if !ok {
|
|
lname = ""
|
|
}
|
|
desc, ok := traits["desc"]
|
|
if !ok {
|
|
desc = ""
|
|
}
|
|
return userTraits{
|
|
Email: email.(string),
|
|
FirstName: fname.(string),
|
|
LastName: lname.(string),
|
|
Description: desc.(string),
|
|
}
|
|
}
|
|
|
|
// Map roles to accounts
|
|
func (s *userService) createUserRoleRelations(ctx context.Context, user *userv3.User, ids parsedIds) (*userv3.User, error) {
|
|
projectNamespaceRoles := user.GetSpec().GetProjectNamespaceRoles()
|
|
|
|
// TODO: add transactions
|
|
var panrs []models.ProjectAccountNamespaceRole
|
|
var pars []models.ProjectAccountResourcerole
|
|
var ars []models.AccountResourcerole
|
|
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 user, fmt.Errorf("unable to find role '%v'", role)
|
|
}
|
|
var roleId uuid.UUID
|
|
if rle, ok := entity.(*models.Role); ok {
|
|
roleId = rle.ID
|
|
} else {
|
|
return user, fmt.Errorf("unable to find role '%v'", role)
|
|
}
|
|
|
|
project := pnr.GetProject()
|
|
org := user.GetMetadata().GetOrganization()
|
|
namespaceId := pnr.GetNamespace() // TODO: lookup id from name
|
|
|
|
switch {
|
|
case pnr.Namespace != nil:
|
|
projectId, err := pg.GetProjectId(ctx, s.db, project)
|
|
if err != nil {
|
|
return user, fmt.Errorf("unable to find project '%v'", project)
|
|
}
|
|
panr := models.ProjectAccountNamespaceRole{
|
|
CreatedAt: time.Now(),
|
|
ModifiedAt: time.Now(),
|
|
Trash: false,
|
|
RoleId: roleId,
|
|
PartnerId: ids.Partner,
|
|
OrganizationId: ids.Organization,
|
|
AccountId: ids.Id,
|
|
ProjectId: projectId,
|
|
NamespaceId: namespaceId,
|
|
Active: true,
|
|
}
|
|
panrs = append(panrs, panr)
|
|
|
|
ps = append(ps, &authzv1.Policy{
|
|
Sub: "u:" + user.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 user, fmt.Errorf("unable to find project '%v'", project)
|
|
}
|
|
par := models.ProjectAccountResourcerole{
|
|
CreatedAt: time.Now(),
|
|
ModifiedAt: time.Now(),
|
|
Trash: false,
|
|
Default: true,
|
|
RoleId: roleId,
|
|
PartnerId: ids.Partner,
|
|
OrganizationId: ids.Organization,
|
|
AccountId: ids.Id,
|
|
ProjectId: projectId,
|
|
Active: true,
|
|
}
|
|
pars = append(pars, par)
|
|
|
|
ps = append(ps, &authzv1.Policy{
|
|
Sub: "u:" + user.GetMetadata().GetName(),
|
|
Ns: "*",
|
|
Proj: project,
|
|
Org: org,
|
|
Obj: role,
|
|
Act: "*",
|
|
})
|
|
default:
|
|
ar := models.AccountResourcerole{
|
|
CreatedAt: time.Now(),
|
|
ModifiedAt: time.Now(),
|
|
Trash: false,
|
|
Default: true,
|
|
RoleId: roleId,
|
|
PartnerId: ids.Partner,
|
|
OrganizationId: ids.Organization,
|
|
AccountId: ids.Id,
|
|
Active: true,
|
|
}
|
|
ars = append(ars, ar)
|
|
|
|
ps = append(ps, &authzv1.Policy{
|
|
Sub: "u:" + user.GetMetadata().GetName(),
|
|
Ns: "*",
|
|
Proj: "*",
|
|
Org: org,
|
|
Obj: role,
|
|
Act: "*",
|
|
})
|
|
}
|
|
}
|
|
if len(panrs) > 0 {
|
|
_, err := pg.Create(ctx, s.db, &panrs)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
}
|
|
if len(pars) > 0 {
|
|
_, err := pg.Create(ctx, s.db, &pars)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
}
|
|
if len(ars) > 0 {
|
|
_, err := pg.Create(ctx, s.db, &ars)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
}
|
|
|
|
if len(ps) > 0 {
|
|
success, err := s.azc.CreatePolicies(ctx, &authzv1.Policies{Policies: ps})
|
|
if err != nil || !success.Res {
|
|
return &userv3.User{}, fmt.Errorf("unable to create mapping in authz; %v", err)
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// FIXME: make this generic
|
|
func (s *userService) getPartnerOrganization(ctx context.Context, user *userv3.User) (uuid.UUID, uuid.UUID, error) {
|
|
partner := user.GetMetadata().GetPartner()
|
|
org := user.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 *userService) Create(ctx context.Context, user *userv3.User) (*userv3.User, error) {
|
|
// TODO: restrict endpoint to admin
|
|
partnerId, organizationId, err := s.getPartnerOrganization(ctx, user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get partner and org id")
|
|
}
|
|
|
|
// Kratos checks if the user is already available
|
|
id, err := s.ap.Create(ctx, map[string]interface{}{
|
|
"email": user.GetMetadata().GetName(), // can be just username for API access
|
|
"first_name": user.GetSpec().GetFirstName(),
|
|
"last_name": user.GetSpec().GetLastName(),
|
|
"description": user.GetMetadata().GetDescription(),
|
|
})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
uid, _ := uuid.Parse(id)
|
|
user, err = s.createUserRoleRelations(ctx, user, parsedIds{Id: uid, Partner: partnerId, Organization: organizationId})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
rl, err := s.ap.GetRecoveryLink(ctx, id)
|
|
fmt.Println("Recovery link:", rl) // TODO: email the recovery link to the user
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (s *userService) identitiesModelToUser(ctx context.Context, user *userv3.User, usr *models.KratosIdentities) (*userv3.User, error) {
|
|
traits := getUserTraits(usr.Traits)
|
|
groups, err := dao.GetGroups(ctx, s.db, usr.ID)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
groupNames := []string{}
|
|
for _, g := range groups {
|
|
groupNames = append(groupNames, g.Name)
|
|
}
|
|
|
|
labels := make(map[string]string)
|
|
|
|
roles, err := dao.GetUserRoles(ctx, s.db, usr.ID)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
user.ApiVersion = apiVersion
|
|
user.Kind = userKind
|
|
user.Metadata = &v3.Metadata{
|
|
Name: traits.Email,
|
|
Description: traits.Description,
|
|
Labels: labels,
|
|
ModifiedAt: timestamppb.New(usr.UpdatedAt),
|
|
}
|
|
user.Spec = &userv3.UserSpec{
|
|
FirstName: traits.FirstName,
|
|
LastName: traits.LastName,
|
|
Groups: groupNames,
|
|
ProjectNamespaceRoles: roles,
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (s *userService) GetByID(ctx context.Context, user *userv3.User) (*userv3.User, error) {
|
|
id := user.GetMetadata().GetId()
|
|
uid, err := uuid.Parse(id)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
entity, err := pg.GetByID(ctx, s.db, uid, &models.KratosIdentities{})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
if usr, ok := entity.(*models.KratosIdentities); ok {
|
|
user, err := s.identitiesModelToUser(ctx, user, usr)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
return user, fmt.Errorf("unabele to fetch user '%v'", id)
|
|
|
|
}
|
|
|
|
func (s *userService) GetByName(ctx context.Context, user *userv3.User) (*userv3.User, error) {
|
|
name := user.GetMetadata().GetName()
|
|
entity, err := pg.GetByTraits(ctx, s.db, name, &models.KratosIdentities{})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
if usr, ok := entity.(*models.KratosIdentities); ok {
|
|
user, err := s.identitiesModelToUser(ctx, user, usr)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
fmt.Println("user:", user)
|
|
return user, nil
|
|
}
|
|
|
|
func (s *userService) deleteUserRoleRelations(ctx context.Context, userId uuid.UUID, user *userv3.User) error {
|
|
err := pg.DeleteX(ctx, s.db, "account_id", userId, &models.AccountResourcerole{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = pg.DeleteX(ctx, s.db, "account_id", userId, &models.ProjectAccountResourcerole{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = pg.DeleteX(ctx, s.db, "account_id", userId, &models.ProjectAccountNamespaceRole{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = s.azc.DeletePolicies(ctx, &authzv1.Policy{Sub: "u:" + user.GetMetadata().GetName()})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete user-role relations from authz; %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.User, error) {
|
|
name := user.GetMetadata().GetName()
|
|
entity, err := pg.GetIdByTraits(ctx, s.db, name, &models.KratosIdentities{})
|
|
if err != nil {
|
|
return &userv3.User{}, fmt.Errorf("no user found with name '%v'", name)
|
|
}
|
|
|
|
if usr, ok := entity.(*models.KratosIdentities); ok {
|
|
partnerId, organizationId, err := s.getPartnerOrganization(ctx, user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get partner and org id")
|
|
}
|
|
err = s.ap.Update(ctx, usr.ID.String(), map[string]interface{}{
|
|
"email": user.GetMetadata().GetName(),
|
|
"first_name": user.GetSpec().GetFirstName(),
|
|
"last_name": user.GetSpec().GetLastName(),
|
|
"description": user.GetMetadata().GetDescription(),
|
|
})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
err = s.deleteUserRoleRelations(ctx, usr.ID, user)
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
|
|
user, err = s.createUserRoleRelations(ctx, user, parsedIds{Id: usr.ID, Partner: partnerId, Organization: organizationId})
|
|
if err != nil {
|
|
return &userv3.User{}, err
|
|
}
|
|
} else {
|
|
return &userv3.User{}, fmt.Errorf("unable to update user '%v'", name)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (s *userService) Delete(ctx context.Context, user *userv3.User) (*userrpcv3.DeleteUserResponse, error) {
|
|
name := user.GetMetadata().GetName()
|
|
entity, err := pg.GetIdByTraits(ctx, s.db, name, &models.KratosIdentities{})
|
|
if err != nil {
|
|
return &userrpcv3.DeleteUserResponse{}, fmt.Errorf("no user founnd with username '%v'", name)
|
|
}
|
|
|
|
if usr, ok := entity.(*models.KratosIdentities); ok {
|
|
err = s.deleteUserRoleRelations(ctx, usr.ID, user)
|
|
if err != nil {
|
|
return &userrpcv3.DeleteUserResponse{}, err
|
|
}
|
|
|
|
err := s.ap.Delete(ctx, usr.ID.String())
|
|
if err != nil {
|
|
return &userrpcv3.DeleteUserResponse{}, err
|
|
}
|
|
|
|
err = pg.DeleteX(ctx, s.db, "account_id", usr.ID, &models.GroupAccount{})
|
|
if err != nil {
|
|
return &userrpcv3.DeleteUserResponse{}, fmt.Errorf("unable to delete user; %v", err)
|
|
}
|
|
|
|
return &userrpcv3.DeleteUserResponse{}, nil
|
|
}
|
|
return &userrpcv3.DeleteUserResponse{}, fmt.Errorf("unable to delete user '%v'", user.Metadata.Name)
|
|
|
|
}
|
|
|
|
func (s *userService) List(ctx context.Context, _ *userv3.User) (*userv3.UserList, error) {
|
|
var users []*userv3.User
|
|
userList := &userv3.UserList{
|
|
ApiVersion: apiVersion,
|
|
Kind: userListKind,
|
|
Metadata: &v3.ListMetadata{
|
|
Count: 0,
|
|
},
|
|
}
|
|
var accs []models.KratosIdentities
|
|
entities, err := pg.ListAll(ctx, s.db, &accs)
|
|
if err != nil {
|
|
return userList, err
|
|
}
|
|
if usrs, ok := entities.(*[]models.KratosIdentities); ok {
|
|
for _, usr := range *usrs {
|
|
user := &userv3.User{}
|
|
user, err := s.identitiesModelToUser(ctx, user, &usr)
|
|
if err != nil {
|
|
return userList, err
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
// update the list metadata and items response
|
|
userList.Metadata = &v3.ListMetadata{
|
|
Count: int64(len(users)),
|
|
}
|
|
userList.Items = users
|
|
}
|
|
|
|
return userList, nil
|
|
}
|
|
|
|
func (s *userService) RetrieveCliConfig(ctx context.Context, req *userrpcv3.ApiKeyRequest) (*common.CliConfigDownloadData, error) {
|
|
// get the default project associated to this account
|
|
ap, err := dao.GetDefaultAccountProject(ctx, s.db, uuid.MustParse(req.Id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// fetch the metadata information required to populate cli config
|
|
var proj models.Project
|
|
_, err = pg.GetByID(ctx, s.db, ap.ProjecttId, &proj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var org models.Organization
|
|
_, err = pg.GetByID(ctx, s.db, ap.OrganizationId, &org)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var part models.Partner
|
|
_, err = pg.GetByID(ctx, s.db, ap.PartnerId, &part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get the api key if exists, if not create a new one
|
|
apikey, err := s.ks.Get(ctx, &userrpcv3.ApiKeyRequest{Username: req.Username})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if apikey == nil {
|
|
apikey, err = s.ks.Create(ctx, &userrpcv3.ApiKeyRequest{Username: req.Username, Id: req.Id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cliConfig := &common.CliConfigDownloadData{
|
|
Profile: s.cc.Profile,
|
|
RestEndpoint: s.cc.RestEndpoint,
|
|
OpsEndpoint: s.cc.OpsEndpoint,
|
|
ApiKey: apikey.Key,
|
|
ApiSecret: apikey.Secret,
|
|
Project: proj.Name,
|
|
Organization: org.Name,
|
|
Partner: part.Name,
|
|
}
|
|
|
|
return cliConfig, nil
|
|
|
|
}
|