Skip to content

Commit 207c18a

Browse files
authored
Merge pull request #4667 from communitybridge/unicron-impersonation-investigation
Adding APIs: /v2/user-from-session, /v4/user-from-token
2 parents 6f5c9c0 + 0601eb4 commit 207c18a

File tree

13 files changed

+337
-13
lines changed

13 files changed

+337
-13
lines changed

cla-backend-go/cmd/server.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package cmd
55

66
import (
7+
"context"
78
"encoding/json"
89
"errors"
910
"fmt"
@@ -97,6 +98,7 @@ import (
9798
"github.com/communitybridge/easycla/cla-backend-go/user"
9899
v2ClaManager "github.com/communitybridge/easycla/cla-backend-go/v2/cla_manager"
99100
v2Company "github.com/communitybridge/easycla/cla-backend-go/v2/company"
101+
v2CurrentUser "github.com/communitybridge/easycla/cla-backend-go/v2/current_user"
100102
v2Health "github.com/communitybridge/easycla/cla-backend-go/v2/health"
101103
"github.com/communitybridge/easycla/cla-backend-go/v2/store"
102104
v2Template "github.com/communitybridge/easycla/cla-backend-go/v2/template"
@@ -296,6 +298,7 @@ func server(localMode bool) http.Handler {
296298
v2ProjectService := v2Project.NewService(v1ProjectService, v1CLAGroupRepo, v1ProjectClaGroupRepo)
297299
v1CompanyService := v1Company.NewService(v1CompanyRepo, configFile.CorporateConsoleV1URL, userRepo, usersService)
298300
v2CompanyService := v2Company.NewService(v1CompanyService, signaturesRepo, v1CLAGroupRepo, usersRepo, v1CompanyRepo, v1ProjectClaGroupRepo, eventsService)
301+
v2CurrentUserService := v2CurrentUser.NewService()
299302

300303
v1RepositoriesService := v1Repositories.NewService(gitV1Repository, githubOrganizationsRepo, v1ProjectClaGroupRepo)
301304
v2RepositoriesService := v2Repositories.NewService(gitV1Repository, gitV2Repository, v1ProjectClaGroupRepo, githubOrganizationsRepo, gitlabOrganizationRepo, eventsService)
@@ -358,6 +361,7 @@ func server(localMode bool) http.Handler {
358361
gerrits.Configure(api, gerritService, v1ProjectService, eventsService)
359362
v2Gerrits.Configure(v2API, gerritService, v1ProjectService, eventsService, v1ProjectClaGroupRepo)
360363
v2Company.Configure(v2API, v2CompanyService, v1ProjectClaGroupRepo, configFile.LFXPortalURL, configFile.CorporateConsoleV1URL)
364+
v2CurrentUser.Configure(v2API, v2CurrentUserService)
361365
cla_manager.Configure(api, v1ClaManagerService, v1CompanyService, v1ProjectService, usersService, v1SignaturesService, eventsService, emailTemplateService)
362366
v2ClaManager.Configure(v2API, v2ClaManagerService, v1CompanyService, configFile.LFXPortalURL, configFile.CorporateConsoleV2URL, v1ProjectClaGroupRepo, userRepo)
363367
cla_groups.Configure(v2API, v2ClaGroupService, v1ProjectService, v1ProjectClaGroupRepo, eventsService)
@@ -371,7 +375,7 @@ func server(localMode bool) http.Handler {
371375

372376
userCreaterMiddleware := func(next http.Handler) http.Handler {
373377
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
374-
createUserFromRequest(authorizer, usersService, eventsService, r)
378+
r = createUserFromRequest(authorizer, usersService, eventsService, r)
375379
next.ServeHTTP(w, r)
376380
})
377381
}
@@ -610,46 +614,53 @@ func responseLoggingMiddleware(next http.Handler) http.Handler {
610614

611615
// create user form http authorization token
612616
// this function creates user if user does not exist and token is valid
613-
func createUserFromRequest(authorizer auth.Authorizer, usersService users.Service, eventsService events.Service, r *http.Request) {
617+
func createUserFromRequest(authorizer auth.Authorizer, usersService users.Service, eventsService events.Service, r *http.Request) *http.Request {
614618
f := logrus.Fields{
615619
"functionName": "cmd.createUserFromRequest",
616620
}
617621

618622
bToken := r.Header.Get("Authorization")
619623
if bToken == "" {
620-
return
624+
return r
621625
}
622626
t := strings.Split(bToken, " ")
623627
if len(t) != 2 {
624628
log.WithFields(f).Warn("parsing of authorization header failed - expected two values separated by a space")
625-
return
629+
return r
626630
}
627631

628632
// parse user from the auth token
629633
claUser, err := authorizer.SecurityAuth(t[1], []string{})
630634
if err != nil {
631635
log.WithFields(f).WithError(err).Warn("parsing failed")
632-
return
636+
return r
633637
}
634638
f["claUserName"] = claUser.Name
635639
f["claUserID"] = claUser.UserID
636640
f["claUserLFUsername"] = claUser.LFUsername
637641
f["claUserLFEmail"] = claUser.LFEmail
638642
f["claUserEmails"] = strings.Join(claUser.Emails, ",")
639643

644+
// only needed if API called is /v4/user-from-token
645+
needToStoreUser := r.URL.Path == "/v4/user-from-token"
646+
640647
// search if user exist in database by username
641648
userModel, err := usersService.GetUserByLFUserName(claUser.LFUsername)
642649
if err != nil {
643650
if _, ok := err.(*utils.UserNotFound); ok {
644651
log.WithFields(f).Debug("unable to locate user by lf-email")
645652
} else {
646653
log.WithFields(f).WithError(err).Warn("searching user by lf-username failed")
647-
return
654+
return r
648655
}
649656
}
650657
// If found - just return
651658
if userModel != nil {
652-
return
659+
if !needToStoreUser {
660+
return r
661+
}
662+
ctx := context.WithValue(r.Context(), "user", userModel) // nolint
663+
return r.WithContext(ctx)
653664
}
654665

655666
// search if user exist in database by username
@@ -659,12 +670,16 @@ func createUserFromRequest(authorizer auth.Authorizer, usersService users.Servic
659670
log.WithFields(f).Debug("unable to locate user by lf-email")
660671
} else {
661672
log.WithFields(f).WithError(err).Warn("searching user by lf-email failed")
662-
return
673+
return r
663674
}
664675
}
665676
// If found - just return
666677
if userModel != nil {
667-
return
678+
if !needToStoreUser {
679+
return r
680+
}
681+
ctx := context.WithValue(r.Context(), "user", userModel) // nolint
682+
return r.WithContext(ctx)
668683
}
669684

670685
// Attempt to create the user
@@ -677,7 +692,7 @@ func createUserFromRequest(authorizer auth.Authorizer, usersService users.Servic
677692
userModel, err = usersService.CreateUser(newUser, nil)
678693
if err != nil {
679694
log.WithFields(f).WithField("user", newUser).WithError(err).Warn("creating new user failed")
680-
return
695+
return r
681696
}
682697

683698
// Log the event
@@ -687,4 +702,9 @@ func createUserFromRequest(authorizer auth.Authorizer, usersService users.Servic
687702
UserModel: userModel,
688703
EventData: &events.UserCreatedEventData{},
689704
})
705+
if !needToStoreUser {
706+
return r
707+
}
708+
ctx := context.WithValue(r.Context(), "user", userModel) // nolint
709+
return r.WithContext(ctx)
690710
}

cla-backend-go/swagger/cla.v2.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,32 @@ paths:
26522652
tags:
26532653
- cla-manager
26542654

2655+
/user-from-token:
2656+
get:
2657+
summary: Get user object from current bearer token
2658+
description: Get user object from current bearer token
2659+
operationId: getUserFromToken
2660+
parameters:
2661+
- $ref: "#/parameters/x-request-id"
2662+
- $ref: "#/parameters/x-acl"
2663+
- $ref: "#/parameters/x-username"
2664+
- $ref: "#/parameters/x-email"
2665+
responses:
2666+
'200':
2667+
description: 'Success'
2668+
headers:
2669+
x-request-id:
2670+
type: string
2671+
description: The unique request ID value - assigned/set by the API Gateway based on the session
2672+
schema:
2673+
$ref: '#/definitions/user'
2674+
'400':
2675+
$ref: '#/responses/invalid-request'
2676+
'404':
2677+
$ref: '#/responses/not-found'
2678+
tags:
2679+
- current_user
2680+
26552681
/user/{userID}/request-company-admin:
26562682
post:
26572683
summary: Request Manager
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright The Linux Foundation and each contributor to CommunityBridge.
2+
// SPDX-License-Identifier: MIT
3+
4+
package current_user
5+
6+
import (
7+
"context"
8+
9+
log "github.com/communitybridge/easycla/cla-backend-go/logging"
10+
11+
"github.com/sirupsen/logrus"
12+
13+
"github.com/LF-Engineering/lfx-kit/auth"
14+
"github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations"
15+
"github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/current_user"
16+
"github.com/communitybridge/easycla/cla-backend-go/utils"
17+
"github.com/go-openapi/runtime/middleware"
18+
)
19+
20+
// Configure sets up the middleware handlers
21+
func Configure(api *operations.EasyclaAPI, service Service) { // nolint
22+
api.CurrentUserGetUserFromTokenHandler = current_user.GetUserFromTokenHandlerFunc(
23+
func(params current_user.GetUserFromTokenParams, authUser *auth.User) middleware.Responder {
24+
reqID := utils.GetRequestID(params.XREQUESTID)
25+
ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint
26+
utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL)
27+
f := logrus.Fields{
28+
"functionName": "v2.current_user.handlers.GetUserFromToken",
29+
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
30+
"authUserName": utils.StringValue(params.XUSERNAME),
31+
"authUserEmail": utils.StringValue(params.XEMAIL),
32+
}
33+
log.WithFields(f).Debugf("looking for user from bearer token")
34+
userModel, err := service.UserFromContext(ctx)
35+
if err != nil {
36+
msg := "unable to lookup user from token"
37+
log.WithFields(f).WithError(err).Warn(msg)
38+
return current_user.NewGetUserFromTokenNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg))
39+
}
40+
return current_user.NewGetUserFromTokenOK().WithXRequestID(reqID).WithPayload(userModel)
41+
})
42+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright The Linux Foundation and each contributor to CommunityBridge.
2+
// SPDX-License-Identifier: MIT
3+
4+
package current_user
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models"
11+
v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models"
12+
log "github.com/communitybridge/easycla/cla-backend-go/logging"
13+
"github.com/communitybridge/easycla/cla-backend-go/utils"
14+
"github.com/jinzhu/copier"
15+
"github.com/sirupsen/logrus"
16+
)
17+
18+
type service struct{}
19+
20+
// Service functions for current_user
21+
type Service interface {
22+
UserFromContext(ctx context.Context) (*v2Models.User, error)
23+
}
24+
25+
// NewService returns instance of current_user service
26+
func NewService() Service {
27+
return &service{}
28+
}
29+
30+
func (s *service) UserFromContext(ctx context.Context) (*v2Models.User, error) {
31+
f := logrus.Fields{
32+
"functionName": "v2.current_user.service.UserFromContext",
33+
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
34+
}
35+
36+
ctxUserModel, ok := ctx.Value("user").(*v1Models.User)
37+
if !ok || ctxUserModel == nil {
38+
msg := "unable to lookup user from context"
39+
log.WithFields(f).Warn(msg)
40+
return nil, fmt.Errorf("cannot find user data in context")
41+
}
42+
43+
var v2UserModel v2Models.User
44+
copyErr := copier.Copy(&v2UserModel, &ctxUserModel)
45+
if copyErr != nil {
46+
log.WithFields(f).Warnf("problem converting DB user model to a v2 user model, error: %+v", copyErr)
47+
return nil, copyErr
48+
}
49+
50+
return &v2UserModel, nil
51+
}

cla-backend/cla/controllers/repository_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,17 @@ def sign_request(provider, installation_id, github_repository_id, change_request
3333
"""
3434
service = cla.utils.get_repository_service(provider)
3535
return service.sign_request(installation_id, github_repository_id, change_request_id, request)
36+
37+
def user_from_session(request, response=None):
38+
"""
39+
Return user from OAuth2 session
40+
"""
41+
# LG: to test using MockGitHub class
42+
# import os
43+
# from cla.models.github_models import MockGitHub
44+
# user = MockGitHub(os.environ["GITHUB_OAUTH_TOKEN"]).user_from_session(request)
45+
user = cla.utils.get_repository_service('github').user_from_session(request)
46+
if user is None:
47+
response.status = HTTP_404
48+
return {"errors": "Cannot find user from session"}
49+
return user.to_dict()

cla-backend/cla/models/github_models.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ def received_activity(self, data):
9696
else:
9797
cla.log.debug("github_models.received_activity - Ignoring unsupported action: {}".format(data["action"]))
9898

99+
def user_from_session(self, request):
100+
fn = "github_models.user_from_session" # function name
101+
cla.log.debug(f"{fn} - Loading session from request: {request}...")
102+
session = self._get_request_session(request)
103+
if "github_oauth2_token" in session:
104+
cla.log.debug(f"{fn} - Using existing session GitHub OAuth2 token")
105+
user = self.get_or_create_user(request)
106+
cla.log.debug(f"{fn} - loaded user {user.to_dict()}")
107+
return user
108+
else:
109+
cla.log.debug(f"{fn} - No existing GitHub OAuth2 token - building authorization url and state")
110+
authorization_url, state = self.get_authorization_url_and_state(None, None, None, ["user:email"])
111+
cla.log.debug(f"{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...")
112+
session["github_oauth2_state"] = state
113+
cla.log.debug(f"{fn} - GitHub OAuth2 request with state {state} - sending user to {authorization_url}")
114+
raise falcon.HTTPFound(authorization_url)
115+
99116
def sign_request(self, installation_id, github_repository_id, change_request_id, request):
100117
"""
101118
This method gets called when the OAuth2 app (NOT the GitHub App) needs to get info on the
@@ -1836,10 +1853,12 @@ def _get_request_session(self, request) -> dict:
18361853
return {}
18371854

18381855
def get_user_data(self, session, client_id) -> dict:
1839-
return {"email": "[email protected]", "name": "Test User", "id": 123}
1856+
return {"email": "[email protected]", "name": "Test User", "login": "testuser", "id": 123}
18401857

18411858
def get_user_emails(self, session, client_id):
1842-
return [{"email": "[email protected]", "verified": True, "primary": True, "visibility": "public"}]
1859+
# LG: updated MockGitHub to return emails in the same way as GitHub class
1860+
return ["[email protected]"]
1861+
# return [{"email": "[email protected]", "verified": True, "primary": True, "visibility": "public"}]
18431862

18441863
def get_pull_request(self, github_repository_id, pull_request_number, installation_id):
18451864
return MockGitHubPullRequest(pull_request_number)

cla-backend/cla/routes.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,37 @@ def get_event(event_id: hug.types.text, response):
18041804
"""
18051805
return cla.controllers.event.get_event(event_id=event_id, response=response)
18061806

1807+
@hug.get("/user-from-session", versions=2)
1808+
def user_from_session(request, response):
1809+
"""
1810+
GET: /user-from-session
1811+
Example: https://api.dev.lfcla.com/v2/user-from-session
1812+
Returns user object from OAuth2 session
1813+
Example user returned:
1814+
{
1815+
"date_created": "2025-05-21T08:05:19.221609+0000",
1816+
"date_modified": "2025-05-21T08:09:19.943455+0000",
1817+
"lf_email": null,
1818+
"lf_sub": null,
1819+
"lf_username": null,
1820+
"note": null,
1821+
"user_company_id": null,
1822+
"user_emails": [
1823+
1824+
],
1825+
"user_external_id": null,
1826+
"user_github_id": "123",
1827+
"user_github_username": "testuser",
1828+
"user_gitlab_id": null,
1829+
"user_gitlab_username": null,
1830+
"user_id": "78bb15eb-8cbf-44e6-8b64-c713db6ee51b",
1831+
"user_ldap_id": null,
1832+
"user_name": "Test User",
1833+
"version": "v1"
1834+
}
1835+
"""
1836+
return cla.controllers.repository_service.user_from_session(request, response)
1837+
18071838

18081839
@hug.post("/events", versions=1)
18091840
def create_event(

utils/active_signature_py.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
# user_id='9dcf5bbc-2492-11ed-97c7-3e2a23ea20b5'
3+
# select key, data:expire from fivetran_ingest.dynamodb_product_us_east1_dev.cla_dev_store where key like 'active_signature:%' order by data:expire desc limit 1;
4+
# select key, data:expire from fivetran_ingest.dynamodb_product_us_east_1.cla_prod_store where key like 'active_signature:%' order by data:expire desc limit 1;
5+
# API_URL=https://api.lfcla.dev.platform.linuxfoundation.org DEBUG=1 ./utils/active_signature_py.sh '4b344ac4-f8d9-11ed-ac9b-b29c4ace74e9'
6+
# API_URL=https://api.lfcla.dev.platform.linuxfoundation.org DEBUG=1 ./utils/active_signature_py.sh '4b344ac4-f8d9-11ed-ac9b-b29c4ace74e9'
7+
# API_URL=https://api.easycla.lfx.linuxfoundation.org DEBUG='' ./utils/active_signature_py.sh '564e571e-12d7-4857-abd4-898939accdd7'
8+
9+
if [ -z "$1" ]
10+
then
11+
echo "$0: you need to specify user_id as a 1st parameter"
12+
exit 1
13+
fi
14+
export user_id="$1"
15+
16+
if [ -z "$API_URL" ]
17+
then
18+
export API_URL="http://localhost:5000"
19+
fi
20+
21+
if [ ! -z "$DEBUG" ]
22+
then
23+
echo "curl -s -XGET -H 'Content-Type: application/json' \"${API_URL}/v2/user/${user_id}/active-signature\""
24+
curl -s -XGET -H "Content-Type: application/json" "${API_URL}/v2/user/${user_id}/active-signature"
25+
else
26+
curl -s -XGET -H "Content-Type: application/json" "${API_URL}/v2/user/${user_id}/active-signature" | jq -r '.'
27+
fi

utils/check_prepare_employee_signature_py.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# user_id='9dcf5bbc-2492-11ed-97c7-3e2a23ea20b5'
44
# company_id='862ff296-6508-4f10-9147-2bc2dd7bfe80'
55
# project_id='88ee12de-122b-4c46-9046-19422054ed8d'
6-
# DEBUG=1 ./utils/check_prepare_employee_signature_py 9dcf5bbc-2492-11ed-97c7-3e2a23ea20b5 862ff296-6508-4f10-9147-2bc2dd7bfe80 88ee12de-122b-4c46-9046-19422054ed8d github 'http://localhost'
6+
# DEBUG=1 ./utils/check_prepare_employee_signature_py 9dcf5bbc-2492-11ed-97c7-3e2a23ea20b5 862ff296-6508-4f10-9147-2bc2dd7bfe80 88ee12de-122b-4c46-9046-19422054ed8d
77
# DEBUG=1 ./utils/check_prepare_employee_signature_py.sh 65d22813-1ac0-4292-bb68-fdcb278473a5 4930fe6e-e023-4f56-9767-6f1996a7b730 43c546ff-bc79-4a32-9454-77dabd6afaee
88

99
if [ -z "$1" ]

0 commit comments

Comments
 (0)