lib/dam/info_release.go (191 lines of code) (raw):

// Copyright 2020 Google LLC. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dam import ( "fmt" "net/http" "strings" "time" "google.golang.org/grpc/codes" /* copybara-comment */ "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 */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/timeutil" /* copybara-comment: timeutil */ glog "github.com/golang/glog" /* copybara-comment */ pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/dam/v1" /* copybara-comment: go_proto */ cspb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/store/consents" /* copybara-comment: go_proto */ ) const ( rememberedConsentExpires = 90 * 24 * time.Hour ) func (s *Service) hydraConsentRememberConsentOrInformationReleasePage(consent *hydraapi.ConsentRequest, stateID string, state *pb.ResourceTokenRequestState, tx storage.Tx) (*htmlPageOrRedirectURL, error) { rcp := &cspb.RememberedConsentPreference{} err := s.store.ReadTx(storage.RememberedConsentDatatype, state.Realm, state.Subject, state.Subject, storage.LatestRev, rcp, tx) if err != nil && !storage.ErrNotFound(err) { return nil, status.Errorf(codes.Internal, "read remembered consent failed: %v", err) } found := err == nil if found && rcp.ExpireTime.Seconds < time.Now().Unix() { err := s.store.DeleteTx(storage.RememberedConsentDatatype, state.Realm, state.Subject, state.Subject, storage.LatestRev, tx) if err != nil { return nil, status.Errorf(codes.Internal, "delete expired remembered consent failed: %v", err) } found = false } // has valid remembered consent if found && s.useHydra { return s.acceptHydraConsent(stateID, state, tx) } return s.informationReleasePage(consent, stateID, state) } func (s *Service) informationReleasePage(consent *hydraapi.ConsentRequest, stateID string, state *pb.ResourceTokenRequestState) (*htmlPageOrRedirectURL, error) { args := toInformationReleaseArgs(consent, stateID, state, s.consentDashboardURL) sb := &strings.Builder{} if err := s.infomationReleasePageTmpl.Execute(sb, args); err != nil { return nil, status.Errorf(codes.Internal, "generate information release page failed: %v", err) } return &htmlPageOrRedirectURL{page: sb.String()}, nil } func toInformationReleaseArgs(consent *hydraapi.ConsentRequest, stateID string, state *pb.ResourceTokenRequestState, consentDashboardURL string) *informationReleaseArgs { dashboardURL := strings.ReplaceAll(consentDashboardURL, "${USER_ID}", consent.Subject) args := &informationReleaseArgs{ AssetDir: assetPath, ApplicationName: consent.Client.Name, State: stateID, ID: consent.Subject, Offline: stringset.Contains(state.RequestedScope, "offline"), IsDataset: len(state.Resources) > 0, ConsentDashboardURL: dashboardURL, } if args.IsDataset { for _, ds := range state.Resources { n := fmt.Sprintf("%s/%s/%s/%s", ds.Resource, ds.View, ds.Role, ds.Interface) args.Information = append(args.Information, n) } } else { args.Information = state.Identities } return args } type informationReleaseArgs struct { AssetDir string ApplicationName string State string ID string Offline bool IsDataset bool Information []string ConsentDashboardURL string } // AcceptInformationRelease is the HTTP handler for "dam/inforelease/accept" endpoint. func (s *Service) AcceptInformationRelease(w http.ResponseWriter, r *http.Request) { r.ParseForm() challenge, redirect, err := s.acceptInformationRelease(r) if err == nil { httputils.WriteRedirect(w, r, redirect) return } if s.useHydra && len(challenge) > 0 { hydra.SendConsentReject(w, r, s.httpClient, s.hydraAdminURL, challenge, err) } else { httputils.WriteError(w, err) } } // acceptInformationRelease returns challenge, redirect and status error func (s *Service) acceptInformationRelease(r *http.Request) (_, _ string, ferr error) { stateID := httputils.QueryParam(r, "state") if len(stateID) == 0 { return "", "", status.Errorf(codes.InvalidArgument, "missing %q parameter", "state") } rememberOpt := httputils.QueryParam(r, "remember") remember := false switch rememberOpt { case "remember-release-any": remember = true case "remember-none": remember = false default: return "", "", status.Errorf(codes.InvalidArgument, "unknown remember option %q", rememberOpt) } tx, err := s.store.Tx(true) if err != nil { return "", "", status.Errorf(codes.Unavailable, "accept info release transaction creation failed: %v", err) } defer func() { err := tx.Finish() if ferr == nil && err != nil { ferr = status.Errorf(codes.Internal, "accept info release transaction finish failed: %v", err) } }() state := &pb.ResourceTokenRequestState{} err = s.store.ReadTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, tx) if err != nil { return "", "", status.Errorf(codes.Internal, "accept info release datastore read failed: %v", err) } challenge := state.ConsentChallenge // if the consent need to be remembered now := time.Now() if remember { rcp := &cspb.RememberedConsentPreference{ ClientName: state.ClientName, CreateTime: timeutil.TimestampProto(now), ExpireTime: timeutil.TimestampProto(now.Add(rememberedConsentExpires)), RequestMatchType: cspb.RememberedConsentPreference_ANYTHING, ReleaseType: cspb.RememberedConsentPreference_ANYTHING_NEEDED, } err = s.store.WriteTx(storage.RememberedConsentDatatype, state.Realm, state.Subject, state.Subject, storage.LatestRev, rcp, nil, tx) if err != nil { return challenge, "", status.Errorf(codes.Internal, "accept info release datastore write remember consent failed: %v", err) } } htmlPageOrRedirect, err := s.acceptHydraConsent(stateID, state, tx) if err != nil { return challenge, "", err } return challenge, htmlPageOrRedirect.redirect, nil } // RejectInformationRelease is the HTTP handler for "dam/inforelease/reject" endpoint. func (s *Service) RejectInformationRelease(w http.ResponseWriter, r *http.Request) { r.ParseForm() challenge, err := s.rejectInformationRelease(r) if err == nil { glog.Errorln("rejectInformationRelease() should return err") err = status.Errorf(codes.Internal, "unknown err from rejectInformationRelease()") } if s.useHydra && len(challenge) > 0 { hydra.SendConsentReject(w, r, s.httpClient, s.hydraAdminURL, challenge, err) } else { httputils.WriteError(w, err) } } // rejectInformationRelease returns the challenge and error message. func (s *Service) rejectInformationRelease(r *http.Request) (_ string, ferr error) { stateID := httputils.QueryParam(r, "state") if len(stateID) == 0 { return "", status.Errorf(codes.InvalidArgument, "missing %q parameter", "state") } tx, err := s.store.Tx(true) if err != nil { return "", status.Errorf(codes.Unavailable, "reject info release transaction creation failed: %v", err) } defer func() { err := tx.Finish() if ferr == nil && err != nil { ferr = status.Errorf(codes.Internal, "reject info release transaction finish failed: %v", err) } }() state := &pb.ResourceTokenRequestState{} err = s.store.ReadTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, tx) if err != nil { return "", status.Errorf(codes.Internal, "reject info release datastore read failed: %v", err) } challenge := state.ConsentChallenge // The temporary state for information releasing process can be only used once. err = s.store.DeleteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, tx) if err != nil { return challenge, status.Errorf(codes.Internal, "reject info release datastore delete failed: %v", err) } 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, } }