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
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
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
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
238 filteredRes := make([]*model.User, 0)
239
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
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
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
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