Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BACK-2780] Add new user profiles endpoint. #698

Open
wants to merge 62 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
7b25f7a
Refactoring and adding shoreline models to platform/user for auth.
lostlevels Feb 7, 2024
76a6bb5
Cleanup.
lostlevels Feb 7, 2024
0b9f170
Add /v1/profiles/:userId route.
lostlevels Feb 12, 2024
a93f797
Renaming.
lostlevels Feb 12, 2024
5dbd8ac
Add the route.
lostlevels Feb 12, 2024
6a9014c
Fix build.
lostlevels Feb 12, 2024
ed826a9
Fix test.
lostlevels Feb 12, 2024
2a49fb0
Use permissions like in seagull.
lostlevels Feb 14, 2024
b1fdbd3
Validate the profile.
lostlevels Feb 15, 2024
73cb026
HasWritePermissions.
lostlevels Feb 21, 2024
7d32881
Rename profile routes to be consistent w/ existing ones.
lostlevels Mar 17, 2024
5d56462
Use snakecase attributes for now but don't flatten yet as blip is still
lostlevels Mar 27, 2024
82db89b
Have a LegacyUserProfile to support seagull requests.
lostlevels Apr 1, 2024
9e9f798
Change leagcy profile routes for simpler proxying in routetable.
lostlevels Apr 1, 2024
650b693
Add legacy delete route.
lostlevels Apr 1, 2024
523b23e
Move keycloak client and keycloak user_accessor to own package.
lostlevels Apr 1, 2024
d17ab19
Rename to package keycloak.
lostlevels Apr 1, 2024
e468cbb
Add user profile config for keycloak 24+.
lostlevels Apr 1, 2024
36570c4
Remove user profile config as that's handled in TF.
lostlevels Apr 2, 2024
deb91a7
Add custodian field to profile.
lostlevels Apr 2, 2024
4f40f4b
Allow services to retrieve user profile.
lostlevels Apr 11, 2024
49c2809
Use "dummy" attribute "profile_has_custodian" for easier keycloak
lostlevels Apr 16, 2024
b0b6163
patient.fullName is only set for fake children.
lostlevels Apr 19, 2024
97ce75c
Remove "profile_" prefix from profile keycloak attributes. Add
lostlevels Apr 23, 2024
cdb875c
Use right json.
lostlevels Apr 29, 2024
62a9368
Delete unused shoreline code. Move user.FullUser into user.User.
lostlevels Apr 30, 2024
5efb6ee
Remove unused hasher code.
lostlevels Apr 30, 2024
4670d48
Remove unused fields.
lostlevels Apr 30, 2024
992ea71
Add MRN attribute.
lostlevels Apr 30, 2024
904e276
Copy amoeba's permissions with regards to membership and custodian.
lostlevels May 1, 2024
41a1fae
Remove check from route since part of middleware now.
lostlevels May 1, 2024
8e5598f
Remove unneeded comment.
lostlevels May 29, 2024
d3f829a
Add GroupsForUser as a prelude to some seagull / gatekeeper
lostlevels Jun 5, 2024
942e210
Commence "old" seagull routes that retrieves from the seagull collection
lostlevels Jun 5, 2024
97d74df
Update migration status.
lostlevels Jun 5, 2024
6721784
Make sure seagull.value field is preserved properly during updates and
lostlevels Jun 7, 2024
1354fa9
Use fallback profile accessor to check for profile first in seagull.
lostlevels Jun 11, 2024
3195e7b
Rename repository for clarity of purpose.
lostlevels Jun 11, 2024
e993dd1
Bump gocloak.
lostlevels Jun 19, 2024
3f9eb22
role field.
lostlevels Jun 19, 2024
af7234f
Omit profile fields if empty in response.
lostlevels Jun 20, 2024
196a609
Add clinic profile fields.
lostlevels Jun 24, 2024
c0a9513
Add normalizer methods for profiles.
lostlevels Jun 25, 2024
07508bb
Account for empty profile fullName.
lostlevels Jul 8, 2024
50f373b
[BACK-3046] Create initial shared users with profiles path w/o
lostlevels Jul 10, 2024
55f0007
Start metadata/users/:userid/users filter params.
lostlevels Jul 10, 2024
c8d9d7f
Parse users profiles query filter.
lostlevels Jul 10, 2024
a2bfbbb
Update users route to properly filter out users. Document Permission /
lostlevels Jul 15, 2024
c80234d
Remove unused query filter on users profiles.
lostlevels Jul 17, 2024
2247fc0
Handle email and emails in legacy seagull profiles.
lostlevels Jul 30, 2024
d78c148
Read raw value as map from seagull value.
lostlevels Jul 31, 2024
3401464
Allow setting of profile on seagull document's value field.
lostlevels Jul 31, 2024
e4f607e
Migrate diagnosisType.
lostlevels Jul 31, 2024
f4316d6
Remove email field from clinic as confirmed only a few fake clinic pr…
lostlevels Aug 6, 2024
83222e2
Use correct FullName in case of fake children.
lostlevels Aug 7, 2024
621062b
Handle certain incorrect types in legacy seagull profile.
lostlevels Aug 8, 2024
2908e3c
Fix some logic and tests for profiles.
lostlevels Aug 12, 2024
aba0355
Set max profile field length to equal keycloak < 24
lostlevels Aug 12, 2024
47e7e45
Make some profile values pointers so that some legacy migration profiles
lostlevels Aug 14, 2024
7bd8a85
Export MaxProfileFieldLen
lostlevels Aug 14, 2024
eeee757
Remove unused field, add tests, synchronize keycloak access.
lostlevels Aug 15, 2024
2b1f2fb
Use existing UsersArray type, add update tests.
lostlevels Aug 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions auth/service/api/v1/permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package v1

import (
"net/http"

"github.com/ant0ine/go-json-rest/rest"

"github.com/tidepool-org/platform/request"
"github.com/tidepool-org/platform/service/api"
)

// requireUserHasCustodian aborts with an error if a a request isn't
// authenticated as a user and the user does not have custodian access to the
// user with the id defined in the url param targetParamUserID
func (r *Router) requireUserHasCustodian(targetParamUserID string, handlerFunc rest.HandlerFunc) rest.HandlerFunc {
fn := func(res rest.ResponseWriter, req *rest.Request) {
if handlerFunc != nil && res != nil && req != nil {
targetUserID := req.PathParam(targetParamUserID)
responder := request.MustNewResponder(res, req)
ctx := req.Context()
details := request.GetAuthDetails(ctx)
hasPerms, err := r.PermissionsClient().HasCustodianPermissions(ctx, details.UserID(), targetUserID)
if err != nil {
responder.InternalServerError(err)
return
}
if !hasPerms {
responder.Empty(http.StatusForbidden)
return
}
handlerFunc(res, req)
}
}
return api.RequireUser(fn)
}

// requireWriteAccess aborts with an error if the request isn't a server request
// or the authenticated user doesn't have access to the user id in the url param,
// targetParamUserID
func (r *Router) requireWriteAccess(targetParamUserID string, handlerFunc rest.HandlerFunc) rest.HandlerFunc {
return func(res rest.ResponseWriter, req *rest.Request) {
if handlerFunc != nil && res != nil && req != nil {
targetUserID := req.PathParam(targetParamUserID)
responder := request.MustNewResponder(res, req)
ctx := req.Context()
details := request.GetAuthDetails(ctx)
if details == nil {
responder.Empty(http.StatusUnauthorized)
return
}
if details.IsService() {
handlerFunc(res, req)
return
}
hasPerms, err := r.PermissionsClient().HasWritePermissions(ctx, details.UserID(), targetUserID)
if err != nil {
responder.InternalServerError(err)
return
}
if !hasPerms {
responder.Empty(http.StatusForbidden)
return
}
handlerFunc(res, req)
}
}
}
146 changes: 146 additions & 0 deletions auth/service/api/v1/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package v1

import (
stdErrs "errors"
"net/http"

"github.com/ant0ine/go-json-rest/rest"

"github.com/tidepool-org/platform/request"
"github.com/tidepool-org/platform/service/api"
structValidator "github.com/tidepool-org/platform/structure/validator"
"github.com/tidepool-org/platform/user"
)

func (r *Router) ProfileRoutes() []*rest.Route {
return []*rest.Route{
rest.Get("/v1/users/:userId/profile", api.RequireAuth(r.GetProfile)),
rest.Get("/v1/users/legacy/:userId/profile", api.RequireAuth(r.GetLegacyProfile)),
// The following modification routes required custodian access in seagull, but I'm not sure that's quite right - it seems it should be if the user can modify the userId.
lostlevels marked this conversation as resolved.
Show resolved Hide resolved
rest.Put("/v1/users/:userId/profile", r.requireWriteAccess("userId", r.UpdateProfile)),
lostlevels marked this conversation as resolved.
Show resolved Hide resolved
rest.Put("/v1/users/legacy/:userId/profile", r.requireWriteAccess("userId", r.UpdateLegacyProfile)),
rest.Delete("/v1/users/:userId/profile", r.requireWriteAccess("userId", r.DeleteProfile)),
rest.Delete("/v1/users/legacy/:userId/profile", r.requireWriteAccess("userId", r.DeleteProfile)),
}
}

func (r *Router) GetProfile(res rest.ResponseWriter, req *rest.Request) {
responder := request.MustNewResponder(res, req)
ctx := req.Context()
details := request.GetAuthDetails(ctx)
userID := req.PathParam("userId")

if details.IsUser() {
hasPerms, err := r.PermissionsClient().HasMembershipRelationship(ctx, details.UserID(), userID)
if err != nil {
responder.InternalServerError(err)
return
}
if !hasPerms {
responder.Empty(http.StatusForbidden)
return
}
}

user, err := r.UserAccessor().FindUserById(ctx, userID)
if err != nil {
responder.Error(http.StatusBadRequest, err)
return
}
if user == nil || user.Profile == nil {
responder.Empty(http.StatusNotFound)
return
}

responder.Data(http.StatusOK, user.Profile)
}

func (r *Router) GetLegacyProfile(res rest.ResponseWriter, req *rest.Request) {
responder := request.MustNewResponder(res, req)
ctx := req.Context()
details := request.GetAuthDetails(ctx)
userID := req.PathParam("userId")

if details.IsUser() {
hasPerms, err := r.PermissionsClient().HasMembershipRelationship(ctx, details.UserID(), userID)
if err != nil {
responder.InternalServerError(err)
return
}
if !hasPerms {
responder.Empty(http.StatusForbidden)
return
}
}

user, err := r.UserAccessor().FindUserById(ctx, userID)
if err != nil {
responder.Error(http.StatusBadRequest, err)
return
}
if user == nil || user.Profile == nil {
responder.Empty(http.StatusNotFound)
return
}

responder.Data(http.StatusOK, user.Profile.ToLegacyProfile())
}

func (r *Router) UpdateProfile(res rest.ResponseWriter, req *rest.Request) {
responder := request.MustNewResponder(res, req)

profile := &user.UserProfile{}
if err := request.DecodeRequestBody(req.Request, profile); err != nil {
responder.Error(http.StatusBadRequest, err)
return
}
r.updateProfile(res, req, profile)
}

func (r *Router) UpdateLegacyProfile(res rest.ResponseWriter, req *rest.Request) {
responder := request.MustNewResponder(res, req)

profile := &user.LegacyUserProfile{}
if err := request.DecodeRequestBody(req.Request, profile); err != nil {
responder.Error(http.StatusBadRequest, err)
return
}
r.updateProfile(res, req, profile.ToUserProfile())
}

func (r *Router) updateProfile(res rest.ResponseWriter, req *rest.Request, profile *user.UserProfile) {
responder := request.MustNewResponder(res, req)
ctx := req.Context()
userID := req.PathParam("userId")
if err := structValidator.New().Validate(profile); err != nil {
responder.Error(http.StatusBadRequest, err)
return
}
err := r.UserAccessor().UpdateUserProfile(ctx, userID, profile)
if stdErrs.Is(err, user.ErrUserNotFound) {
responder.Empty(http.StatusNotFound)
return
}
if err != nil {
responder.InternalServerError(err)
return
}
responder.Empty(http.StatusOK)
}

func (r *Router) DeleteProfile(res rest.ResponseWriter, req *rest.Request) {
responder := request.MustNewResponder(res, req)
ctx := req.Context()
userID := req.PathParam("userId")

err := r.UserAccessor().DeleteUserProfile(ctx, userID)
if stdErrs.Is(err, user.ErrUserNotFound) {
responder.Empty(http.StatusNotFound)
return
}
if err != nil {
responder.InternalServerError(err)
return
}
responder.Empty(http.StatusOK)
}
1 change: 1 addition & 0 deletions auth/service/api/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (r *Router) Routes() []*rest.Route {
r.ProviderSessionsRoutes(),
r.RestrictedTokensRoutes(),
r.DeviceCheckRoutes(),
r.ProfileRoutes(),
}
acc := make([]*rest.Route, 0)
for _, r := range routes {
Expand Down
4 changes: 4 additions & 0 deletions auth/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"

"github.com/tidepool-org/platform/apple"
"github.com/tidepool-org/platform/user"

confirmationClient "github.com/tidepool-org/hydrophone/client"

"github.com/tidepool-org/platform/auth/store"
permission "github.com/tidepool-org/platform/permission"
"github.com/tidepool-org/platform/provider"
"github.com/tidepool-org/platform/service"
"github.com/tidepool-org/platform/task"
Expand All @@ -18,6 +20,8 @@ type Service interface {

Domain() string
AuthStore() store.Store
UserAccessor() user.UserAccessor
PermissionsClient() permission.Client

ProviderFactory() provider.Factory

Expand Down
50 changes: 50 additions & 0 deletions auth/service/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/tidepool-org/platform/apple"
"github.com/tidepool-org/platform/auth"
"github.com/tidepool-org/platform/user"
"github.com/tidepool-org/platform/user/keycloak"

eventsCommon "github.com/tidepool-org/go-common/events"

Expand All @@ -28,6 +30,8 @@ import (
"github.com/tidepool-org/platform/errors"
"github.com/tidepool-org/platform/events"
logInternal "github.com/tidepool-org/platform/log"
"github.com/tidepool-org/platform/permission"
permissionClient "github.com/tidepool-org/platform/permission/client"
"github.com/tidepool-org/platform/platform"
"github.com/tidepool-org/platform/provider"
providerFactory "github.com/tidepool-org/platform/provider/factory"
Expand Down Expand Up @@ -56,6 +60,8 @@ type Service struct {
authClient *Client
userEventsHandler events.Runner
deviceCheck apple.DeviceCheck
userAccessor user.UserAccessor
permsClient *permissionClient.Client
}

func New() *Service {
Expand Down Expand Up @@ -108,6 +114,12 @@ func (s *Service) Initialize(provider application.Provider) error {
if err := s.initializeDeviceCheck(); err != nil {
return err
}
if err := s.initializeUserAccessor(); err != nil {
return err
}
if err := s.initializePermissionsClient(); err != nil {
return err
}
return s.initializeUserEventsHandler()
}

Expand Down Expand Up @@ -152,6 +164,13 @@ func (s *Service) DeviceCheck() apple.DeviceCheck {
return s.deviceCheck
}

func (s *Service) UserAccessor() user.UserAccessor {
return s.userAccessor
}

func (s *Service) PermissionsClient() permission.Client {
return s.permsClient
}
func (s *Service) Status(ctx context.Context) *service.Status {
return &service.Status{
Version: s.VersionReporter().Long(),
Expand Down Expand Up @@ -325,6 +344,25 @@ func (s *Service) initializeTaskClient() error {
return nil
}

func (s *Service) initializePermissionsClient() error {
s.Logger().Debug("Loading permission client config")

cfg := platform.NewConfig()
cfg.UserAgent = s.UserAgent()
reporter := s.ConfigReporter().WithScopes("permission", "client")
loader := platform.NewConfigReporterLoader(reporter)
if err := cfg.Load(loader); err != nil {
return errors.Wrap(err, "unable to load permission client config")
}

permsClient, err := permissionClient.New(cfg, platform.AuthorizeAsService)
if err != nil {
return errors.Wrap(err, "unable to create permission client")
}
s.permsClient = permsClient
return nil
}

func (s *Service) terminateTaskClient() {
if s.taskClient != nil {
s.Logger().Debug("Destroying task client")
Expand Down Expand Up @@ -416,6 +454,18 @@ func (s *Service) initializeUserEventsHandler() error {
return nil
}

func (s *Service) initializeUserAccessor() error {
s.Logger().Debug("Initializing user accessor")

config := &keycloak.KeycloakConfig{}
if err := config.FromEnv(); err != nil {
return err
}
s.userAccessor = keycloak.NewKeycloakUserAccessor(config)

return nil
}

func (s *Service) initializeDeviceCheck() error {
s.Logger().Debug("Initializing device check")

Expand Down
10 changes: 10 additions & 0 deletions auth/service/test/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/tidepool-org/platform/apple"
"github.com/tidepool-org/platform/user"

"github.com/onsi/gomega"

Expand All @@ -12,6 +13,7 @@ import (
"github.com/tidepool-org/platform/auth/service"
"github.com/tidepool-org/platform/auth/store"
authStoreTest "github.com/tidepool-org/platform/auth/store/test"
"github.com/tidepool-org/platform/permission"
"github.com/tidepool-org/platform/provider"
providerTest "github.com/tidepool-org/platform/provider/test"
serviceTest "github.com/tidepool-org/platform/service/test"
Expand Down Expand Up @@ -79,6 +81,14 @@ func (s *Service) DeviceCheck() apple.DeviceCheck {
return nil
}

func (s *Service) UserAccessor() user.UserAccessor {
return nil
}

func (s *Service) PermissionsClient() permission.Client {
return nil
}

func (s *Service) Status(ctx context.Context) *service.Status {
s.StatusInvocations++

Expand Down
Loading