Files
paralus/pkg/service/user.go
Abin Simon df810ab45a Convert from dao interface to funcs
This was done inorder to support transactions which will be done in
the next PR. This is the first step towards that.
2022-03-16 17:10:32 +05:30

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
}