...

Source file src/edge-infra.dev/pkg/edge/api/services/user_management_service.go

Documentation: edge-infra.dev/pkg/edge/api/services

     1  package services
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"edge-infra.dev/pkg/edge/api/apierror"
    14  	"edge-infra.dev/pkg/edge/api/bsl/types"
    15  	"edge-infra.dev/pkg/edge/api/graph/mapper"
    16  	"edge-infra.dev/pkg/edge/api/graph/model"
    17  	"edge-infra.dev/pkg/edge/api/middleware"
    18  	edgetypes "edge-infra.dev/pkg/edge/api/types"
    19  	"edge-infra.dev/pkg/edge/api/utils"
    20  	"edge-infra.dev/pkg/edge/bsl"
    21  	"edge-infra.dev/pkg/edge/okta"
    22  )
    23  
    24  const (
    25  	bspUsersPath                  = "/provisioning/users"
    26  	bspGetUsersPath               = "/provisioning/users?pageNumber=%d&pageSize=%d&userType=PRIMARY"
    27  	bspUserProfilePath            = "/provisioning/user-profiles"
    28  	resetPasswordURLPath          = "/provisioning/users/reset-password"
    29  	ResetPasswordWithTokenURLPath = "/provisioning/user-profiles/reset-password"
    30  	exchangeTokenURLPath          = "/security/security-tokens/exchange"
    31  	bslGetUserPath                = "/provisioning/users/%s"
    32  	oktaIntrospectionPath         = "/v1/introspect?client_id=%s"
    33  	oktaExchangePath              = "/security/authentication/okta-exchange"
    34  	oktaTokenRefreshPath          = "/v1/token"
    35  	oktaUserInfoPath              = "/v1/userinfo"
    36  	effectiveRoles                = "/provisioning/role-grants/user-grants/self/effective-roles"
    37  )
    38  
    39  //go:generate mockgen -destination=../mocks/mock_user_management_service.go -package=mocks edge-infra.dev/pkg/edge/api/services UserManagementService
    40  type UserManagementService interface {
    41  	Login(ctx context.Context, username, password, organization string) (*model.AuthPayload, error)
    42  	Register(ctx context.Context, firstName, lastName, username, email, password, organization string) (string, error)
    43  	TokenExchange(ctx context.Context, organization string, user *types.AuthUser, provider string) (string, error)
    44  	GetUserProfile(ctx context.Context, token string) (*model.User, error)
    45  	GetUsers(ctx context.Context) ([]*model.User, error)
    46  	GetUsersForOrgBanner(ctx context.Context, bannerName string) ([]*model.User, error)
    47  	Delete(ctx context.Context, username string) error
    48  	ResetPassword(ctx context.Context, username, newPassword, organization string) error
    49  	UpdateUserPasswordWithToken(ctx context.Context, token, organization, newPassword string) error
    50  	WhoAmI(ctx context.Context, username, organization, token, provider string) (*model.User, error)
    51  	UpdateUserProfile(ctx context.Context, user *model.UpdateUser) (*model.User, error)
    52  	GetUser(ctx context.Context, username, organization, token, provider string) (*model.User, error)
    53  	VerifyOktaToken(ctx context.Context, token string) (*okta.IntrospectionResponse, error)
    54  	LoginWithOktaToken(ctx context.Context, token, refreshToken, organization string) (*model.OktaAuthPayload, error)
    55  	GetSessionUserEdgeRoles(ctx context.Context, username, token, organization, authProvider string) ([]string, error)
    56  	UserData(ctx context.Context, username string) (*model.UserData, error)
    57  }
    58  
    59  type userManagementService struct {
    60  	Config        edgetypes.Config
    61  	BSLClient     *bsl.Client
    62  	OktaClient    *okta.Client
    63  	RoleService   RoleService
    64  	BannerService BannerService
    65  }
    66  
    67  func (u *userManagementService) Login(ctx context.Context, username, password, organization string) (*model.AuthPayload, error) {
    68  	client, secTokenData, err := u.BSLClient.WithAuthentication(ctx, organization, username, password)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	userProfile, err := u.GetUserProfile(ctx, secTokenData.Token)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	edgeRoles := make([]string, 0)
    79  
    80  	if !secTokenData.CredentialsExpired && utils.Contains(secTokenData.Authorities, "NEP_IDENTITY_VIEWER") {
    81  		//add edge groups to roles
    82  		groups, err := GetGroupsForUser(ctx, client, username)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		edgeRoles = append(edgeRoles, groups...)
    87  	}
    88  
    89  	token, err := middleware.CreateToken(username, utils.CheckString(userProfile.Email), secTokenData.OrganizationName, u.Config.App.AppSecret, edgeRoles, secTokenData.Token, "bsl", "")
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	sessionTime, err := time.ParseDuration(fmt.Sprintf("%ds", secTokenData.MaxSessionTime))
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return &model.AuthPayload{
   100  		Token:              token,
   101  		FirstName:          &userProfile.GivenName,
   102  		FullName:           userProfile.FullName,
   103  		Roles:              edgeRoles,
   104  		CredentialsExpired: secTokenData.CredentialsExpired,
   105  		SessionTime:        sessionTime.Minutes(),
   106  		Organization:       secTokenData.OrganizationName,
   107  	}, nil
   108  }
   109  
   110  func (u *userManagementService) Register(ctx context.Context, firstName, lastName, username, email, password, organization string) (string, error) {
   111  	client, err := u.BSLClient.WithBackendOrgAccessKey(ctx, organization)
   112  	if err != nil {
   113  		return "", err
   114  	}
   115  	User := &model.User{}
   116  	genericErr := client.
   117  		SetPayload(mapper.ToCreateUserRequest(firstName, lastName, username, email, password, "ACTIVE")).
   118  		JSON("Post", bspUsersPath, User)
   119  	if genericErr != nil {
   120  		return "", genericErr
   121  	}
   122  	return User.Username, nil
   123  }
   124  
   125  func (u *userManagementService) TokenExchange(ctx context.Context, organization string, user *types.AuthUser, provider string) (string, error) {
   126  	var (
   127  		err           error
   128  		bslResponse   = &model.AuthPayload{}
   129  		oktaResponse  = &okta.RefreshResponse{}
   130  		responseToken string
   131  	)
   132  	switch provider {
   133  	case model.AuthProviderBsl.String():
   134  		err = u.BSLClient.WithAccessToken(ctx, user.Token).
   135  			SetOrg(organization).
   136  			SetPayload(map[string]string{"token": user.Token}).
   137  			JSON(http.MethodPost, exchangeTokenURLPath, bslResponse)
   138  		if err != nil {
   139  			return "", err
   140  		}
   141  		responseToken = bslResponse.Token
   142  	case model.AuthProviderOkta.String():
   143  		err = u.OktaClient.WithFormData(ctx, map[string]string{
   144  			"grant_type":    "refresh_token",
   145  			"scope":         "offline_access openid",
   146  			"refresh_token": user.RefreshToken,
   147  			"client_id":     u.Config.Okta.ClientID,
   148  		}).Post(oktaTokenRefreshPath, oktaResponse)
   149  		if err != nil {
   150  			return "", err
   151  		}
   152  		responseToken = oktaResponse.AccessToken
   153  	default:
   154  		return "", errors.New("unsupported auth provider")
   155  	}
   156  	token, err := middleware.CreateToken(user.Username, user.Email, user.Organization, u.Config.App.AppSecret, user.Roles, responseToken, user.AuthProvider, oktaResponse.RefreshToken)
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  	return token, nil
   161  }
   162  
   163  func (u *userManagementService) GetUserProfile(ctx context.Context, token string) (*model.User, error) {
   164  	akd := &model.User{}
   165  	err := u.BSLClient.WithAccessToken(ctx, token).JSON("Get", bspUserProfilePath, akd)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return akd, nil
   170  }
   171  
   172  func (u *userManagementService) GetUsers(ctx context.Context) ([]*model.User, error) {
   173  	client := u.BSLClient.WithUserTokenCredentials(ctx)
   174  	data := &types.FindUsersResponse{}
   175  	users := make([]*model.User, 0)
   176  	for pageNumber := 0; !data.LastPage; pageNumber++ {
   177  		if err := client.JSON("Get", fmt.Sprintf(bspGetUsersPath, pageNumber, pageSize), data); err != nil {
   178  			if errors.Is(err, sql.ErrNoRows) {
   179  				continue
   180  			}
   181  			return nil, err
   182  		}
   183  		for _, user := range data.PageContent {
   184  			if strings.HasSuffix(user.Username, fmt.Sprintf("-%s", edgetypes.BFFUsername)) {
   185  				continue
   186  			}
   187  			userEmail := utils.ConvertToString(user.Email)
   188  			users = append(users, &model.User{
   189  				Username:   user.Username,
   190  				FamilyName: user.FamilyName,
   191  				FullName:   user.FullName,
   192  				Status:     user.Status,
   193  				Email:      &userEmail,
   194  			})
   195  		}
   196  	}
   197  
   198  	return users, nil
   199  }
   200  
   201  func (u *userManagementService) GetUsersForOrgBanner(ctx context.Context, bannerName string) ([]*model.User, error) {
   202  	user := middleware.ForContext(ctx)
   203  	subOrg := user.Organization + bannerName
   204  	client := u.BSLClient.WithUserTokenCredentials(ctx)
   205  	client.SetQueryParam("userType", "EXTERNAL")
   206  	client.SetOrg(subOrg)
   207  
   208  	data := &types.FindUsersResponse{}
   209  	userNames := make([]types.UserName, 0)
   210  
   211  	pageNumber := 0
   212  	for !data.LastPage {
   213  		client.SetQueryParams(map[string]string{
   214  			"pageNumber": strconv.Itoa(pageNumber),
   215  			"pageSize":   strconv.Itoa(pageSize),
   216  		})
   217  		if err := client.JSON(http.MethodGet, bspUsersPath, data); err != nil {
   218  			return nil, err
   219  		}
   220  
   221  		for _, d := range data.PageContent {
   222  			userNames = append(userNames, types.UserName{Username: d.Username})
   223  			// fullName still in the acct: + org_name + @ + qdlid here
   224  		}
   225  		pageNumber++
   226  	}
   227  
   228  	client = u.BSLClient.WithUserTokenCredentials(ctx)
   229  	payload := types.GetUserDetailsRequest{UserIDs: userNames}
   230  	res := &types.GetUserDetailsResponse{}
   231  	err := client.SetPayload(payload).JSON(http.MethodPost, bspUserDetailsPath, res)
   232  
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	// Filtering out any FullName that is also a service account
   238  	filteredRes := make([]*model.User, 0)
   239  	// Appending list of user without `acct:` pattern to a new filtered list
   240  	for _, user := range res.Users {
   241  		if !strings.HasPrefix(user.FullName, "acct:") {
   242  			filteredRes = append(filteredRes, user)
   243  		}
   244  	}
   245  	return filteredRes, nil
   246  }
   247  
   248  func (u *userManagementService) Delete(ctx context.Context, username string) error {
   249  	return u.BSLClient.WithUserTokenCredentials(ctx).Delete(fmt.Sprintf("%s/%s", bspUsersPath, username))
   250  }
   251  
   252  func (u *userManagementService) ResetPassword(ctx context.Context, username, newPassword, organization string) error {
   253  	client, err := u.BSLClient.WithBackendOrgAccessKey(ctx, organization)
   254  	if err != nil {
   255  		return apierror.Wrap(err).AddGenericErrorExtension("verbose", err)
   256  	}
   257  	resetRequest := types.ResetPasswordRequest{
   258  		Username: bsl.CreateFullAccountName(&types.AuthUser{Organization: organization, Username: username}),
   259  		Password: newPassword,
   260  	}
   261  	return client.SetPayload(resetRequest).Put(resetPasswordURLPath)
   262  }
   263  
   264  // WhoAmI todo figure this one out
   265  func (u userManagementService) WhoAmI(ctx context.Context, username, organization, token, provider string) (*model.User, error) {
   266  	return u.GetUser(ctx, username, organization, token, provider)
   267  }
   268  
   269  // UpdateUserPasswordWithToken Updates a users password in bsl using a one time use token
   270  func (u userManagementService) UpdateUserPasswordWithToken(ctx context.Context, token, organization, newPassword string) error {
   271  	req := types.ResetPasswordWithTokenRequest{
   272  		NewPassword: newPassword,
   273  	}
   274  	return u.BSLClient.WithAccessToken(ctx, token).SetPayload(req).SetOrg(organization).Put(ResetPasswordWithTokenURLPath)
   275  }
   276  
   277  func (u *userManagementService) UpdateUserProfile(ctx context.Context, input *model.UpdateUser) (*model.User, error) {
   278  	user := &model.User{}
   279  	return user, u.BSLClient.WithUserTokenCredentials(ctx).SetPayload(input).JSON("Put", bspUsersPath, user)
   280  }
   281  
   282  func (u *userManagementService) GetUser(ctx context.Context, username, organization, token, provider string) (*model.User, error) {
   283  	fullyQualifiedUsername := fmt.Sprintf("acct:%s@%s", organization, username)
   284  	if provider != "bsl" {
   285  		response, err := u.getOktaUserInfo(ctx, token)
   286  		if err != nil {
   287  			return nil, err
   288  		}
   289  		fullyQualifiedUsername = fmt.Sprintf("acct:commerce@%s-%s", username, response.PreferredUsername)
   290  	}
   291  	user := &model.User{
   292  		UserData: &model.UserData{},
   293  	}
   294  	if err := u.BSLClient.WithUserTokenCredentials(ctx).JSON(http.MethodGet, fmt.Sprintf(bslGetUserPath, fullyQualifiedUsername), user); err != nil {
   295  		return nil, err
   296  	}
   297  	return user, nil
   298  }
   299  
   300  func (u *userManagementService) UserData(ctx context.Context, username string) (*model.UserData, error) {
   301  	userData := &model.UserData{}
   302  	roles, err := u.RoleService.GetEdgeGroupsForUserUser(ctx, username)
   303  	if err != nil && !errors.Is(err, sql.ErrNoRows) {
   304  		return userData, err
   305  	}
   306  	userData.Roles = roles
   307  	banners, err := u.BannerService.GetUserAssignedBanners(ctx, username)
   308  	if err != nil && !errors.Is(err, sql.ErrNoRows) {
   309  		return userData, err
   310  	}
   311  	userData.AssignedBanners = banners
   312  	return userData, nil
   313  }
   314  
   315  // VerifyOktaToken accepts an okta access token and verifies that it's valid and active
   316  func (u *userManagementService) VerifyOktaToken(ctx context.Context, token string) (*okta.IntrospectionResponse, error) {
   317  	response := &okta.IntrospectionResponse{}
   318  	err := u.OktaClient.WithFormData(ctx, map[string]string{
   319  		"token":           token,
   320  		"token_type_hint": "access_token",
   321  	}).Post(fmt.Sprintf(oktaIntrospectionPath, u.Config.Okta.ClientID), response)
   322  	return response, err
   323  }
   324  
   325  func (u *userManagementService) LoginWithOktaToken(ctx context.Context, token, refreshToken, organization string) (*model.OktaAuthPayload, error) {
   326  	oktaResponse, err := u.VerifyOktaToken(ctx, token)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	if !oktaResponse.Active {
   332  		invalidToken := errors.New("invalid token")
   333  		return nil, invalidToken
   334  	}
   335  
   336  	response, err := u.getOktaUserInfo(ctx, token)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	org := fmt.Sprintf("%s%s/", u.Config.BSP.Root, organization)
   341  	fullyQualifiedUsername := fmt.Sprintf("acct:commerce@%s-%s", response.Sub, response.PreferredUsername)
   342  	roles, err := u.GetOktaRoles(ctx, token, fullyQualifiedUsername, org)
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	return &model.OktaAuthPayload{
   348  		Token:        token,
   349  		RefreshToken: refreshToken,
   350  		FirstName:    &response.GivenName,
   351  		LastName:     &response.FamilyName,
   352  		FullName:     response.Name,
   353  		Username:     response.Sub,
   354  		Email:        response.Email,
   355  		Valid:        oktaResponse.Active,
   356  		SessionTime:  float64(oktaResponse.Expires),
   357  		Organization: org,
   358  		Roles:        roles,
   359  	}, nil
   360  }
   361  
   362  func (u *userManagementService) GetOktaRoles(ctx context.Context, token, username, org string) ([]string, error) {
   363  	resp := &types.EffectiveRolesResponse{}
   364  	roles := make([]string, 0)
   365  	client := u.BSLClient.WithOktaToken(ctx, token).SetOrg(org)
   366  	err := client.JSON(http.MethodGet, effectiveRoles, resp)
   367  	tempRoles := make(map[string]bool)
   368  	for _, effectiveRole := range resp.Content {
   369  		tempRoles[effectiveRole.RoleName] = true
   370  	}
   371  	if _, exists := tempRoles["NEP_IDENTITY_VIEWER"]; exists {
   372  		groups, err := GetGroupsForUser(ctx, client, username)
   373  		if err != nil {
   374  			return nil, err
   375  		}
   376  		roles = append(roles, groups...)
   377  	}
   378  	return roles, err
   379  }
   380  
   381  func (u *userManagementService) GetSessionUserEdgeRoles(ctx context.Context, username, token, organization, authProvider string) ([]string, error) {
   382  	fullyQualifiedUsername := username
   383  	if authProvider != "bsl" {
   384  		response, err := u.getOktaUserInfo(ctx, token)
   385  		if err != nil {
   386  			return nil, err
   387  		}
   388  		fullyQualifiedUsername = fmt.Sprintf("acct:commerce@%s-%s", username, response.PreferredUsername)
   389  	}
   390  
   391  	roles, err := u.GetOktaRoles(ctx, token, fullyQualifiedUsername, organization)
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	return roles, err
   396  }
   397  
   398  func (u *userManagementService) getOktaUserInfo(ctx context.Context, token string) (*okta.UserInfo, error) {
   399  	response := &okta.UserInfo{}
   400  	err := u.OktaClient.WithNoData(ctx).WithAccessToken(token).Get(oktaUserInfoPath, response)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	return response, nil
   405  }
   406  
   407  func NewUserManagementService(config edgetypes.Config, client *bsl.Client, oktaClient *okta.Client, roleService RoleService, bannerService BannerService) UserManagementService {
   408  	return &userManagementService{
   409  		Config:        config,
   410  		BSLClient:     client,
   411  		OktaClient:    oktaClient,
   412  		RoleService:   roleService,
   413  		BannerService: bannerService,
   414  	}
   415  }
   416  

View as plain text