diff --git a/lib/consentsapi/consents.go b/lib/consentsapi/consents.go index 2e37d4c0..9897fc85 100644 --- a/lib/consentsapi/consents.go +++ b/lib/consentsapi/consents.go @@ -20,6 +20,7 @@ import ( "net/http" "regexp" "sort" + "time" "github.com/gorilla/mux" /* copybara-comment */ "google.golang.org/grpc/codes" /* copybara-comment */ @@ -40,14 +41,14 @@ const ( ) var ( - uuidRE = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + uuidRE = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + timeNow = time.Now ) // Service contains store and funcs to access data. type Service struct { - Store storage.Store - FindRememberedConsentsByUser func(store storage.Store, subject, realm, clientName string, offset, pageSize int, tx storage.Tx) (map[string]*storepb.RememberedConsentPreference, error) - Clients func(tx storage.Tx) (map[string]*cpb.Client, error) + Store storage.Store + Clients func(tx storage.Tx) (map[string]*cpb.Client, error) } // ListConsentsFactory http handler for "/identity/v1alpha/{realm}/users/{user}/consents" @@ -75,7 +76,7 @@ func (s *listConsentsHandler) Setup(r *http.Request, tx storage.Tx) (int, error) userID := mux.Vars(r)["user"] realm := mux.Vars(r)["realm"] - rcs, err := s.s.FindRememberedConsentsByUser(s.s.Store, userID, realm, "", 0, maxRememberedConsent, tx) + rcs, err := findRememberedConsentsByUser(s.s.Store, userID, realm, "", 0, maxRememberedConsent, tx) if err != nil { return httputils.FromError(err), err } @@ -151,18 +152,22 @@ func toConsentVisas(list []*storepb.RememberedConsentPreference_Visa) []*cspb.Co } // DeleteConsentFactory http handler for "/identity/v1alpha/{realm}/users/{user}/consents/{consent_id}" -func DeleteConsentFactory(serv *Service, consentPath string) *handlerfactory.Options { - return &handlerfactory.Options{ +func DeleteConsentFactory(serv *Service, consentPath string, consentIDUseUUID bool) *handlerfactory.Options { + opts := &handlerfactory.Options{ TypeName: "consent", PathPrefix: consentPath, HasNamedIdentifiers: false, - NameChecker: map[string]*regexp.Regexp{ - "consent_id": uuidRE, - }, Service: func() handlerfactory.Service { return &deleteConsentHandler{s: serv} }, } + + if consentIDUseUUID { + opts.NameChecker = map[string]*regexp.Regexp{ + "consent_id": uuidRE, + } + } + return opts } type deleteConsentHandler struct { @@ -191,3 +196,37 @@ func (s *deleteConsentHandler) Save(r *http.Request, tx storage.Tx, name string, } return nil } + +// findRememberedConsentsByUser returns all RememberedConsents of user of client. +func findRememberedConsentsByUser(store storage.Store, subject, realm, clientName string, offset, pageSize int, tx storage.Tx) (map[string]*storepb.RememberedConsentPreference, error) { + content := make(map[string]map[string]proto.Message) + count, err := store.MultiReadTx(storage.RememberedConsentDatatype, realm, subject, nil, offset, pageSize, content, &storepb.RememberedConsentPreference{}, tx) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "findRememberedConsentsByUser MultiReadTx() failed: %v", err) + } + + res := map[string]*storepb.RememberedConsentPreference{} + if count == 0 { + return res, nil + } + + now := timeNow().Unix() + for k, v := range content[subject] { + rcp, ok := v.(*storepb.RememberedConsentPreference) + if !ok { + return nil, status.Errorf(codes.Internal, "findRememberedConsentsByUser obj type incorrect: user=%s, id=%s", subject, k) + } + // remove expired items + if rcp.ExpireTime.Seconds < now { + continue + } + // filter for clientName + if len(clientName) > 0 && rcp.ClientName != clientName { + continue + } + + res[k] = rcp + } + + return res, nil +} diff --git a/lib/consentsapi/consents_test.go b/lib/consentsapi/consents_test.go index 4ba9b4b9..bf0485fe 100644 --- a/lib/consentsapi/consents_test.go +++ b/lib/consentsapi/consents_test.go @@ -35,15 +35,7 @@ import ( ) func TestListConsents(t *testing.T) { - stub := &stub{} - store := fakestore.New() - - handler := handlerfactory.MakeHandler(store, ListConsentsFactory(&Service{ - Store: store, - FindRememberedConsentsByUser: stub.findRememberedConsentsByUser, - Clients: stub.clients, - }, "/identity/v1alpha/{realm}/users/{user}/consents")) - + timeNow = func() time.Time { return time.Time{} } time1 := timeutil.TimestampProto(time.Time{}.Add(100 * time.Hour)) time2 := timeutil.TimestampProto(time.Time{}.Add(200 * time.Hour)) consents := map[string]*storepb.RememberedConsentPreference{ @@ -213,10 +205,19 @@ func TestListConsents(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - stub.consents = tc.consents + stub := &stub{} + store := fakestore.New() + + handler := handlerfactory.MakeHandler(store, ListConsentsFactory(&Service{ + Store: store, + Clients: stub.clients, + }, "/identity/v1alpha/{realm}/users/{user}/consents")) + stub.clis = tc.clients - r := httptest.NewRequest(http.MethodGet, "/identity/v1alpha/masterusers/user1/consents", nil) + storeRememberedConsents(t, store, "user1", storage.DefaultRealm, tc.consents) + + r := httptest.NewRequest(http.MethodGet, "/identity/v1alpha/master/user1/consents", nil) r = mux.SetURLVars(r, map[string]string{ "user": "user1", "realm": "master", @@ -244,10 +245,9 @@ func TestDeleteConsent(t *testing.T) { store := fakestore.New() handler := handlerfactory.MakeHandler(store, DeleteConsentFactory(&Service{ - Store: store, - FindRememberedConsentsByUser: stub.findRememberedConsentsByUser, - Clients: stub.clients, - }, "/identity/v1alpha/{realm}/users/{user}/consents/{consent_id}")) + Store: store, + Clients: stub.clients, + }, "/identity/v1alpha/{realm}/users/{user}/consents/{consent_id}", true)) consentID := "00000000-0000-0000-0000-000000000001" invalidConsentID := "00000000-0000-0000-0000-000000000000" @@ -292,13 +292,17 @@ func TestDeleteConsent(t *testing.T) { } } -type stub struct { - consents map[string]*storepb.RememberedConsentPreference - clis map[string]*cpb.Client +func storeRememberedConsents(t *testing.T, store storage.Store, subject, realm string, consents map[string]*storepb.RememberedConsentPreference) { + t.Helper() + for id, rcp := range consents { + if err := store.Write(storage.RememberedConsentDatatype, realm, subject, id, storage.LatestRev, rcp, nil); err != nil { + t.Fatalf("store RememberedConsentData failed: %v", err) + } + } } -func (s *stub) findRememberedConsentsByUser(store storage.Store, subject, realm, clientName string, offset, pageSize int, tx storage.Tx) (map[string]*storepb.RememberedConsentPreference, error) { - return s.consents, nil +type stub struct { + clis map[string]*cpb.Client } func (s *stub) clients(tx storage.Tx) (map[string]*cpb.Client, error) { diff --git a/lib/dam/dam.go b/lib/dam/dam.go index 3885facf..54476d72 100644 --- a/lib/dam/dam.go +++ b/lib/dam/dam.go @@ -149,9 +149,9 @@ type Options struct { // Store: data storage and configuration storage Store storage.Store // Warehouse: resource token creator service - Warehouse clouds.ResourceTokenCreator + Warehouse clouds.ResourceTokenCreator // AWSClient: a client for interacting with the AWS API - AWSClient aws.APIClient + AWSClient aws.APIClient ServiceAccountManager *saw.AccountWarehouse // Logger: audit log logger Logger *logging.Client @@ -1538,6 +1538,11 @@ func registerHandlers(r *mux.Router, s *Service) { r.HandleFunc(fakeTokenPath, auth.MustWithAuth(faketokensapi.NewTokensHandler(s.tokens).DeleteToken, s.checker, auth.RequireUserToken)).Methods(http.MethodDelete) // consents service endpoints + consentService := s.consentService() + r.HandleFunc(listConsentPath, auth.MustWithAuth(handlerfactory.MakeHandler(s.GetStore(), consentsapi.ListConsentsFactory(consentService, listConsentPath)), s.checker, auth.RequireUserToken)).Methods(http.MethodGet) + r.HandleFunc(deleteConsentPath, auth.MustWithAuth(handlerfactory.MakeHandler(s.GetStore(), consentsapi.DeleteConsentFactory(consentService, deleteConsentPath, false)), s.checker, auth.RequireUserToken)).Methods(http.MethodDelete) + + // TODO: delete the mocked endpoints when complete. consents := &consentsapi.StubConsents{Consent: consentsapi.FakeConsent} r.HandleFunc(consentsPath, auth.MustWithAuth(consentsapi.NewMockConsentsHandler(consents).ListConsents, s.checker, auth.RequireUserToken)).Methods(http.MethodGet) r.HandleFunc(consentPath, auth.MustWithAuth(consentsapi.NewMockConsentsHandler(consents).DeleteConsent, s.checker, auth.RequireUserToken)).Methods(http.MethodDelete) diff --git a/lib/dam/endpoints.go b/lib/dam/endpoints.go index a41263f6..082c5dea 100644 --- a/lib/dam/endpoints.go +++ b/lib/dam/endpoints.go @@ -101,6 +101,9 @@ const ( fakeTokenPath = "/tokens/" // End-point for managing consents. See "proto/tokens/v1/tokens.proto" + listConsentPath = "/dam/v1alpha/{realm}/users/{user}/consents" + deleteConsentPath = "/dam/v1alpha/{realm}/users/{user}/consents/{consent_id}" + // TODO: delete the mocked endpoints when complete. consentsPath = "/consents" consentPath = "/consents/" diff --git a/lib/dam/endpoints_test.go b/lib/dam/endpoints_test.go index 04c141a4..ff1cb3d0 100644 --- a/lib/dam/endpoints_test.go +++ b/lib/dam/endpoints_test.go @@ -51,6 +51,10 @@ var ( "GET /dam/gatekeeper/.well-known/jwks", "GET /dam/gatekeeper/.well-known/openid-configuration", + // consent management endpoints + "GET /dam/v1alpha/{realm}/users/{user}/consents", + "DELETE /dam/v1alpha/{realm}/users/{user}/consents/{consent_id}", + // token management endpoints "GET /dam/v1alpha/users/{user}/tokens", "DELETE /dam/v1alpha/users/{user}/tokens/{token_id}", diff --git a/lib/dam/info_release.go b/lib/dam/info_release.go index 877404ca..d288bf02 100644 --- a/lib/dam/info_release.go +++ b/lib/dam/info_release.go @@ -24,6 +24,7 @@ import ( "google.golang.org/grpc/status" /* copybara-comment */ "bitbucket.org/creachadair/stringset" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/apis/hydraapi" /* copybara-comment: hydraapi */ + "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/consentsapi" /* copybara-comment: consentsapi */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/errutil" /* copybara-comment: errutil */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/hydra" /* copybara-comment: hydra */ @@ -244,3 +245,10 @@ func (s *Service) rejectInformationRelease(r *http.Request) (_ string, ferr erro return challenge, errutil.WithErrorReason("user_denied", status.Errorf(codes.Unauthenticated, "User denied releasing consent")) } + +func (s *Service) consentService() *consentsapi.Service { + return &consentsapi.Service{ + Store: s.store, + Clients: s.clients, + } +} diff --git a/lib/ic/ic.go b/lib/ic/ic.go index d5e0b7a3..706b97e6 100644 --- a/lib/ic/ic.go +++ b/lib/ic/ic.go @@ -1511,7 +1511,7 @@ func registerHandlers(r *mux.Router, s *Service) { // consents service endpoints consentService := s.consentService() r.HandleFunc(listConsentPath, auth.MustWithAuth(handlerfactory.MakeHandler(s.GetStore(), consentsapi.ListConsentsFactory(consentService, listConsentPath)), s.checker, auth.RequireUserToken)).Methods(http.MethodGet) - r.HandleFunc(deleteConsentPath, auth.MustWithAuth(handlerfactory.MakeHandler(s.GetStore(), consentsapi.DeleteConsentFactory(consentService, deleteConsentPath)), s.checker, auth.RequireUserToken)).Methods(http.MethodDelete) + r.HandleFunc(deleteConsentPath, auth.MustWithAuth(handlerfactory.MakeHandler(s.GetStore(), consentsapi.DeleteConsentFactory(consentService, deleteConsentPath, true)), s.checker, auth.RequireUserToken)).Methods(http.MethodDelete) // TODO: delete the mocked endpoints when complete. consents := &consentsapi.StubConsents{Consent: consentsapi.FakeConsent} diff --git a/lib/ic/info_release.go b/lib/ic/info_release.go index a7a8d5f4..2b19c161 100644 --- a/lib/ic/info_release.go +++ b/lib/ic/info_release.go @@ -677,8 +677,7 @@ func (s *Service) clients(tx storage.Tx) (map[string]*cpb.Client, error) { func (s *Service) consentService() *consentsapi.Service { return &consentsapi.Service{ - Store: s.store, - FindRememberedConsentsByUser: findRememberedConsentsByUser, - Clients: s.clients, + Store: s.store, + Clients: s.clients, } }