lib/oathclients/clients.go (154 lines of code) (raw):

// Copyright 2019 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 oathclients contains clients endpoints and helpers related to client credentials. package oathclients import ( "fmt" "net/http" "sort" "strings" "github.com/google/go-cmp/cmp" /* copybara-comment */ "github.com/google/go-cmp/cmp/cmpopts" /* copybara-comment */ "google.golang.org/grpc/codes" /* copybara-comment */ "github.com/go-openapi/strfmt" /* copybara-comment */ "github.com/golang/protobuf/proto" /* copybara-comment */ "google.golang.org/protobuf/testing/protocmp" /* copybara-comment */ "github.com/pborman/uuid" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/apis/hydraapi" /* copybara-comment: hydraapi */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/check" /* copybara-comment: check */ "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/strutil" /* copybara-comment: strutil */ glog "github.com/golang/glog" /* copybara-comment */ pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */ ) const ( cfgClients = "clients" clientIDLen = 36 ) // CheckClientIntegrity check if the given clientHandler integrity. func CheckClientIntegrity(name string, c *pb.Client) error { if err := httputils.CheckName("name", name, nil); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name), fmt.Sprintf("invalid clientHandler name %q: %v", name, err)).Err() } if uid := uuid.Parse(c.ClientId); uid == nil || len(c.ClientId) != clientIDLen { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "clientId"), fmt.Sprintf("missing clientHandler ID or invalid format: %q", c.ClientId)).Err() } if path, err := check.CheckUI(c.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, path), fmt.Sprintf("clientHandler UI settings: %v", err)).Err() } if len(c.RedirectUris) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "RedirectUris"), "missing RedirectUris").Err() } for _, uri := range c.RedirectUris { if strings.HasPrefix(uri, "/") { continue } if !strutil.IsURL(uri) { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "RedirectUris"), fmt.Sprintf("RedirectUris %q is not url", uri)).Err() } } if len(c.Scope) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "Scope"), "missing Scope").Err() } if len(c.GrantTypes) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "GrantTypes"), "missing GrantTypes").Err() } if len(c.ResponseTypes) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgClients, name, "ResponseTypes"), "missing ResponseTypes").Err() } return nil } // ExtractClientID from request. func ExtractClientID(r *http.Request) string { cid := httputils.QueryParam(r, "client_id") if len(cid) > 0 { return cid } return httputils.QueryParam(r, "clientId") } // ExtractClientSecret from request. func ExtractClientSecret(r *http.Request) string { cs := httputils.QueryParam(r, "client_secret") if len(cs) > 0 { return cs } return httputils.QueryParam(r, "clientSecret") } // SyncClients resets clients in hydra with given clients and secrets. func SyncClients(httpClient *http.Client, hydraAdminURL string, clients map[string]*pb.Client, secrets map[string]string) (*pb.ClientState, error) { state, err := SyncState(httpClient, hydraAdminURL, clients, secrets) if err != nil { return nil, err } for name, client := range state.Add { sec := secrets[client.ClientId] thc := toHydraClient(client, name, sec, strfmt.NewDateTime()) if _, err := hydra.CreateClient(httpClient, hydraAdminURL, thc); err != nil { return nil, err } } for name, client := range state.Update { sec := secrets[client.ClientId] thc := toHydraClient(client, name, sec, strfmt.NewDateTime()) if _, err := hydra.UpdateClient(httpClient, hydraAdminURL, thc.ClientID, thc); err != nil { return nil, err } } for _, client := range state.Remove { if err := hydra.DeleteClient(httpClient, hydraAdminURL, client.ClientId); err != nil { return nil, err } } msg := fmt.Sprintf("sync hydra clients: added %d, updated %d, removed %d, unchanged %d, no_secret %d", len(state.Add), len(state.Update), len(state.Remove), len(state.Unchanged), len(state.NoSecret)) state.Status = httputils.NewStatus(codes.OK, msg).Proto() glog.Infof(msg) return state, nil } // SyncState calculates what client sync operations are needed between hydra and the service. func SyncState(httpClient *http.Client, hydraAdminURL string, clients map[string]*pb.Client, secrets map[string]string) (*pb.ClientState, error) { state := &pb.ClientState{ Add: make(map[string]*pb.Client), Update: make(map[string]*pb.Client), UpdateDiff: make(map[string]string), Remove: make(map[string]*pb.Client), Unchanged: make(map[string]*pb.Client), NoSecret: make(map[string]*pb.Client), SecretMismatch: []string{}, } cs, err := hydra.ListClients(httpClient, hydraAdminURL) if err != nil { return nil, err } // Populate existing Hydra clients by ClientID. As the logic handles // these clients, remove them from this map. Remaining items no longer // exist in the Federated Access component, so delete the from Hydra. removable := make(map[string]*hydraapi.Client) for _, c := range cs { removable[c.ClientID] = c } // Add clients to hydra. for n, cli := range clients { c := &pb.Client{} proto.Merge(c, cli) c.Ui = nil sec, ok := secrets[c.ClientId] if !ok { glog.Errorf("sync hydra clients: client %q has no secret, and will not be included in Hydra client list.", n) state.NoSecret[n] = c continue } hc, ok := removable[c.ClientId] if !ok { // Does not exist in hydra, so create. state.Add[n] = c continue } // Update an existing client if it has changed. fhc, hsec := fromHydraClient(hc) if cmp.Equal(fhc, c, protocmp.Transform(), cmpopts.EquateEmpty()) && hsec == sec { state.Unchanged[n] = c } else { state.Update[n] = c if sec != hsec { // Add the name of the client only, do not reveal the secrets in the state. state.SecretMismatch = append(state.SecretMismatch, n) } // Take the diff again without revealing the secrets. state.UpdateDiff[n] = cmp.Diff(fhc, c, protocmp.Transform(), cmpopts.EquateEmpty()) } // Whether updated or unchanged above, remove it from the `removable` list to avoid removing the hydra client below. delete(removable, hc.ClientID) } // Remove remaining existing hydra clients on the `removable` list. for _, hc := range removable { c, _ := fromHydraClient(hc) state.Remove[hc.Name] = c } sort.Strings(state.SecretMismatch) msg := fmt.Sprintf("hydra clients status: add %d, update %d, remove %d, unchanged %d, no_secret %d", len(state.Add), len(state.Update), len(state.Remove), len(state.Unchanged), len(state.NoSecret)) state.Status = httputils.NewStatus(codes.OK, msg).Proto() return state, nil }