lib/consentsapi/consents.go (175 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 consentsapi contains a service manages user's remembered consent
package consentsapi
import (
"fmt"
"net/http"
"regexp"
"sort"
"time"
"github.com/gorilla/mux" /* copybara-comment */
"google.golang.org/grpc/codes" /* copybara-comment */
"google.golang.org/grpc/status" /* copybara-comment */
"github.com/golang/protobuf/proto" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/handlerfactory" /* copybara-comment: handlerfactory */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */
epb "github.com/golang/protobuf/ptypes/empty" /* copybara-comment */
cpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */
cspb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/consents/v1" /* copybara-comment: consents_go_proto */
storepb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/store/consents" /* copybara-comment: go_proto */
)
const (
maxRememberedConsent = 200
)
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}$`)
timeNow = time.Now
)
// Service contains store and funcs to access data.
type Service struct {
Store storage.Store
Clients func(tx storage.Tx) (map[string]*cpb.Client, error)
}
// ListConsentsFactory http handler for "/identity/v1alpha/{realm}/users/{user}/consents"
func ListConsentsFactory(serv *Service, consentsPath string) *handlerfactory.Options {
return &handlerfactory.Options{
TypeName: "consent",
PathPrefix: consentsPath,
HasNamedIdentifiers: false,
Service: func() handlerfactory.Service {
return &listConsentsHandler{s: serv}
},
}
}
type listConsentsHandler struct {
handlerfactory.Empty
s *Service
userID string
clients map[string]*cpb.Client
remembered map[string]*storepb.RememberedConsentPreference
}
func (s *listConsentsHandler) Setup(r *http.Request, tx storage.Tx) (int, error) {
userID := mux.Vars(r)["user"]
realm := mux.Vars(r)["realm"]
rcs, err := findRememberedConsentsByUser(s.s.Store, userID, realm, "", 0, maxRememberedConsent, tx)
if err != nil {
return httputils.FromError(err), err
}
clients, err := s.s.Clients(tx)
if err != nil {
return httputils.FromError(err), err
}
s.userID = userID
s.remembered = rcs
s.clients = clients
return http.StatusOK, nil
}
func (s *listConsentsHandler) Get(r *http.Request, name string) (proto.Message, error) {
return toListConsentsResponse(s.remembered, s.userID, s.clients), nil
}
func toListConsentsResponse(m map[string]*storepb.RememberedConsentPreference, userID string, clients map[string]*cpb.Client) *cspb.ListConsentsResponse {
res := &cspb.ListConsentsResponse{}
for k, v := range m {
res.Consents = append(res.Consents, &cspb.Consent{
Name: fmt.Sprintf("users/%s/consents/%s", userID, k),
Client: toConsentClient(v.ClientName, clients[v.ClientName]),
CreateTime: v.CreateTime,
ExpireTime: v.ExpireTime,
RequestMatchType: cspb.Consent_RequestMatchType(v.RequestMatchType),
RequestedResources: v.RequestedResources,
RequestedScopes: v.RequestedScopes,
ReleaseType: cspb.Consent_ReleaseType(v.ReleaseType),
SelectedVisas: toConsentVisas(v.SelectedVisas),
ReleaseProfileName: v.ReleaseProfileName,
ReleaseProfileEmail: v.ReleaseProfileEmail,
ReleaseProfileOther: v.ReleaseProfileOther,
ReleaseAccountAdmin: v.ReleaseAccountAdmin,
ReleaseLink: v.ReleaseLink,
ReleaseIdentities: v.ReleaseIdentities,
})
}
// order by CreateTime
sort.Slice(res.Consents, func(i int, j int) bool {
return res.Consents[i].CreateTime.Seconds > res.Consents[j].CreateTime.Seconds
})
return res
}
func toConsentClient(name string, client *cpb.Client) *cspb.Consent_Client {
c := &cspb.Consent_Client{Name: name}
if client != nil {
c.ClientId = client.ClientId
c.Ui = client.Ui
}
return c
}
func toConsentVisas(list []*storepb.RememberedConsentPreference_Visa) []*cspb.Consent_Visa {
var res []*cspb.Consent_Visa
for _, v := range list {
res = append(res, &cspb.Consent_Visa{
Type: v.Type,
Source: v.Source,
By: v.By,
Iss: v.Iss,
})
}
return res
}
// DeleteConsentFactory http handler for "/identity/v1alpha/{realm}/users/{user}/consents/{consent_id}"
func DeleteConsentFactory(serv *Service, consentPath string, consentIDUseUUID bool) *handlerfactory.Options {
opts := &handlerfactory.Options{
TypeName: "consent",
PathPrefix: consentPath,
HasNamedIdentifiers: false,
Service: func() handlerfactory.Service {
return &deleteConsentHandler{s: serv}
},
}
if consentIDUseUUID {
opts.NameChecker = map[string]*regexp.Regexp{
"consent_id": uuidRE,
}
}
return opts
}
type deleteConsentHandler struct {
handlerfactory.Empty
s *Service
userID string
realm string
consentID string
}
func (s *deleteConsentHandler) Remove(r *http.Request, name string) (proto.Message, error) {
s.userID = mux.Vars(r)["user"]
s.consentID = mux.Vars(r)["consent_id"]
s.realm = mux.Vars(r)["realm"]
return &epb.Empty{}, nil
}
func (s *deleteConsentHandler) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error {
if err := s.s.Store.DeleteTx(storage.RememberedConsentDatatype, s.realm, s.userID, s.consentID, storage.LatestRev, tx); err != nil {
if storage.ErrNotFound(err) {
return status.Errorf(codes.NotFound, "delete consent item not found")
}
return status.Errorf(codes.Unavailable, "delete consent DeleteTx failed: %v", err)
}
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) {
results, err := store.MultiReadTx(storage.RememberedConsentDatatype, realm, subject, storage.MatchAllIDs, nil, offset, pageSize, &storepb.RememberedConsentPreference{}, tx)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "findRememberedConsentsByUser MultiReadTx() failed: %v", err)
}
res := map[string]*storepb.RememberedConsentPreference{}
if len(results.Entries) == 0 {
return res, nil
}
now := timeNow().Unix()
for _, entry := range results.Entries {
rcp, ok := entry.Item.(*storepb.RememberedConsentPreference)
if !ok {
return nil, status.Errorf(codes.Internal, "findRememberedConsentsByUser obj type incorrect: user=%s, id=%s", subject, entry.ItemID)
}
// remove expired items
if rcp.ExpireTime.Seconds < now {
continue
}
// filter for clientName
if len(clientName) > 0 && rcp.ClientName != clientName {
continue
}
res[entry.ItemID] = rcp
}
return res, nil
}