lib/ic/configadmin.go (414 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 ic import ( "fmt" "net/http" "strconv" "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/check" /* copybara-comment: check */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */ "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 */ cpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */ pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/ic/v1" /* copybara-comment: go_proto */ ) // HTTP handler for ".../config" func (s *Service) configFactory() *handlerfactory.Options { return &handlerfactory.Options{ TypeName: "config", PathPrefix: configPath, HasNamedIdentifiers: false, Service: func() handlerfactory.Service { return &config{ s: s, input: &pb.ConfigRequest{}, } }, } } type config struct { s *Service input *pb.ConfigRequest cfg *pb.IcConfig id *ga4gh.Identity } func (c *config) Setup(r *http.Request, tx storage.Tx) (int, error) { cfg, _, id, status, err := c.s.handlerSetup(tx, r, noScope, c.input) c.cfg = cfg c.id = id return status, err } func (c *config) LookupItem(r *http.Request, name string, vars map[string]string) bool { // Trival name as there is only one config. return true } func (c *config) NormalizeInput(r *http.Request, name string, vars map[string]string) error { if err := httputils.DecodeProtoReq(c.input, r); err != nil { return err } if c.input.Item == nil { c.input.Item = &pb.IcConfig{} } if c.input.Modification == nil { c.input.Modification = &pb.ConfigModification{} } if c.input.Item.IdentityProviders == nil { c.input.Item.IdentityProviders = make(map[string]*cpb.IdentityProvider) } if c.input.Item.Clients == nil { c.input.Item.Clients = make(map[string]*cpb.Client) } if c.input.Item.Options == nil { c.input.Item.Options = &pb.ConfigOptions{} } c.input.Item.Options = receiveConfigOptions(c.input.Item.Options) return nil } func (c *config) Get(r *http.Request, name string) (proto.Message, error) { return makeConfig(c.cfg), nil } func (c *config) Post(r *http.Request, name string) (proto.Message, error) { return nil, fmt.Errorf("POST not allowed") } func (c *config) Put(r *http.Request, name string) (proto.Message, error) { if getRealm(r) != storage.DefaultRealm && !check.ClientsEqual(c.input.Item.Clients, c.cfg.Clients) { return nil, status.Errorf(codes.PermissionDenied, "modify clients is only allowed on master realm") } if c.cfg.Version != c.input.Item.Version { // TODO: consider upgrading older config versions automatically. return nil, fmt.Errorf("PUT of config version %q mismatched with existing config version %q", c.input.Item.Version, c.cfg.Version) } // Retain the revision number (it will be incremented upon saving). c.input.Item.Revision = c.cfg.Revision return nil, nil } func (c *config) Patch(r *http.Request, name string) (proto.Message, error) { return nil, fmt.Errorf("PATCH not allowed") } func (c *config) Remove(r *http.Request, name string) (proto.Message, error) { return nil, fmt.Errorf("DELETE not allowed") } func (c *config) CheckIntegrity(r *http.Request) *status.Status { bad := codes.InvalidArgument if err := check.ValidToWriteConfig(getRealm(r), c.cfg.Options.ReadOnlyMasterRealm); err != nil { return httputils.NewStatus(bad, err.Error()) } if len(c.input.Item.Version) == 0 { return httputils.NewStatus(bad, "missing config version") } if c.input.Item.Revision <= 0 { return httputils.NewStatus(bad, "invalid config revision") } if err := configRevision(c.input.Modification, c.cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } if err := c.s.checkConfigIntegrity(c.input.Item); err != nil { return httputils.NewStatus(bad, err.Error()) } return nil } func (c *config) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error { if err := c.s.saveConfig(c.input.Item, desc, typeName, r, c.id, c.cfg, c.input.Item, c.input.Modification, tx); err != nil { return err } secrets, err := c.s.loadSecrets(tx) if err != nil { return err } // Assumes that secrets don't change within this handler. if c.s.useHydra && !check.ClientsEqual(c.input.Item.Clients, c.cfg.Clients) { if _, err = c.s.syncToHydra(c.input.Item.Clients, secrets.ClientSecrets, 0, tx); err != nil { return err } } return nil } // HTTP handler for ".../config/identityProviders/{name}" func (s *Service) configIdpFactory() *handlerfactory.Options { return &handlerfactory.Options{ TypeName: "configIDP", PathPrefix: configIdentityProvidersPath, HasNamedIdentifiers: true, Service: func() handlerfactory.Service { return &configIDP{ s: s, input: &pb.ConfigIdentityProviderRequest{}, } }, } } type configIDP struct { s *Service r *http.Request input *pb.ConfigIdentityProviderRequest item *cpb.IdentityProvider save *cpb.IdentityProvider sec *pb.IcSecrets saveSecret *pb.IcSecrets cfg *pb.IcConfig id *ga4gh.Identity tx storage.Tx } func (c *configIDP) Setup(r *http.Request, tx storage.Tx) (int, error) { cfg, sec, id, status, err := c.s.handlerSetup(tx, r, noScope, c.input) c.cfg = cfg c.sec = sec c.id = id c.tx = tx if c.sec.IdProviderSecrets == nil { c.sec.IdProviderSecrets = map[string]string{} } return status, err } func (c *configIDP) LookupItem(r *http.Request, name string, vars map[string]string) bool { if item, ok := c.cfg.IdentityProviders[name]; ok { c.item = item return true } return false } func (c *configIDP) NormalizeInput(r *http.Request, name string, vars map[string]string) error { if err := httputils.DecodeProtoReq(c.input, r); err != nil { return err } if c.input.Item == nil { c.input.Item = &cpb.IdentityProvider{} } if c.input.Item.Scopes == nil { c.input.Item.Scopes = []string{} } if c.input.Item.Ui == nil { c.input.Item.Ui = make(map[string]string) } return nil } func (c *configIDP) Get(r *http.Request, name string) (proto.Message, error) { return c.item, nil } func (c *configIDP) Post(r *http.Request, name string) (proto.Message, error) { c.save = c.input.Item c.cfg.IdentityProviders[name] = c.save if err := c.modifyClientSecret(name); err != nil { return nil, err } return nil, nil } func (c *configIDP) Put(r *http.Request, name string) (proto.Message, error) { c.save = c.input.Item c.cfg.IdentityProviders[name] = c.save if err := c.modifyClientSecret(name); err != nil { return nil, err } return nil, nil } func (c *configIDP) Patch(r *http.Request, name string) (proto.Message, error) { c.save = &cpb.IdentityProvider{} proto.Merge(c.save, c.item) proto.Merge(c.save, c.input.Item) c.save.Scopes = c.input.Item.Scopes c.save.Ui = c.input.Item.Ui c.cfg.IdentityProviders[name] = c.save if err := c.modifyClientSecret(name); err != nil { return nil, err } return nil, nil } func (c *configIDP) Remove(r *http.Request, name string) (proto.Message, error) { delete(c.cfg.IdentityProviders, name) c.save = &cpb.IdentityProvider{} if len(c.item.ClientId) > 0 { c.saveSecret = c.sec delete(c.saveSecret.IdProviderSecrets, c.item.ClientId) } return nil, nil } func (c *configIDP) CheckIntegrity(r *http.Request) *status.Status { bad := codes.InvalidArgument if err := check.ValidToWriteConfig(getRealm(r), c.cfg.Options.ReadOnlyMasterRealm); err != nil { return httputils.NewStatus(bad, err.Error()) } if err := configRevision(c.input.Modification, c.cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } if err := c.s.checkConfigIntegrity(c.cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } return nil } func (c *configIDP) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error { if c.input.Modification != nil && c.input.Modification.DryRun { return nil } if c.save != nil { if err := c.s.saveConfig(c.cfg, desc, typeName, r, c.id, c.item, c.save, c.input.Modification, c.tx); err != nil { return err } } if c.saveSecret != nil { if err := c.s.saveSecrets(c.saveSecret, desc, typeName, r, c.id, tx); err != nil { return err } } return nil } // modifyClientSecret when request includes clientSecret and clientId is set. func (c *configIDP) modifyClientSecret(name string) error { if len(c.input.ClientSecret) > 0 { if len(c.save.ClientId) == 0 { return status.Errorf(codes.InvalidArgument, "update trusted issuer %q client_secret but missing client_id", name) } c.saveSecret = c.sec c.saveSecret.IdProviderSecrets[c.input.Item.ClientId] = c.input.ClientSecret } return nil } // HTTP handler for ".../config/options" func (s *Service) configOptionsFactory() *handlerfactory.Options { return &handlerfactory.Options{ TypeName: "configOptions", PathPrefix: configOptionsPath, HasNamedIdentifiers: false, Service: func() handlerfactory.Service { return &configOptions{ s: s, input: &pb.ConfigOptionsRequest{}, } }, } } type configOptions struct { s *Service r *http.Request input *pb.ConfigOptionsRequest item *pb.ConfigOptions save *pb.ConfigOptions cfg *pb.IcConfig id *ga4gh.Identity tx storage.Tx } func (c *configOptions) Setup(r *http.Request, tx storage.Tx) (int, error) { cfg, _, id, status, err := c.s.handlerSetup(tx, r, noScope, c.input) c.cfg = cfg c.id = id c.tx = tx return status, err } func (c *configOptions) LookupItem(r *http.Request, name string, vars map[string]string) bool { c.item = c.cfg.Options return true } func (c *configOptions) NormalizeInput(r *http.Request, name string, vars map[string]string) error { if err := httputils.DecodeProtoReq(c.input, r); err != nil { return err } if c.input.Item == nil { c.input.Item = &pb.ConfigOptions{} } c.input.Item = receiveConfigOptions(c.input.Item) return nil } func (c *configOptions) Get(r *http.Request, name string) (proto.Message, error) { return makeConfigOptions(c.item), nil } func (c *configOptions) Post(r *http.Request, name string) (proto.Message, error) { c.save = c.input.Item c.cfg.Options = c.save return nil, nil } func (c *configOptions) Put(r *http.Request, name string) (proto.Message, error) { c.save = c.input.Item c.cfg.Options = c.save return nil, nil } func (c *configOptions) Patch(r *http.Request, name string) (proto.Message, error) { c.save = &pb.ConfigOptions{} proto.Merge(c.save, c.item) proto.Merge(c.save, c.input.Item) c.save.ReadOnlyMasterRealm = c.input.Item.ReadOnlyMasterRealm c.cfg.Options = c.save return nil, nil } func (c *configOptions) Remove(r *http.Request, name string) (proto.Message, error) { return nil, fmt.Errorf("DELETE not allowed") } func (c *configOptions) CheckIntegrity(r *http.Request) *status.Status { bad := codes.InvalidArgument if err := check.ValidToWriteConfig(getRealm(r), c.cfg.Options.ReadOnlyMasterRealm); err != nil { return httputils.NewStatus(bad, err.Error()) } if err := configRevision(c.input.Modification, c.cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } if err := c.s.checkConfigIntegrity(c.cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } return nil } func (c *configOptions) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error { if c.save == nil || (c.input.Modification != nil && c.input.Modification.DryRun) { return nil } if err := c.s.saveConfig(c.cfg, desc, typeName, r, c.id, c.item, c.save, c.input.Modification, c.tx); err != nil { return err } return nil } // HTTP handler for ".../config/clients/{name}" // .... // HTTP handler for ".../config/options" // ConfigHistory implements the HistoryConfig RPC method. func (s *Service) ConfigHistory(w http.ResponseWriter, r *http.Request) { // TODO: consider requiring an "admin" scope (modify all admin handlerSetup calls). _, _, _, sts, err := s.handlerSetup(nil, r, noScope, nil) if err != nil { httputils.WriteError(w, status.Errorf(httputils.RPCCode(sts), "%v", err)) return } h, sts, err := storage.GetHistory(s.store, storage.ConfigDatatype, getRealm(r), storage.DefaultUser, storage.DefaultID, r) if err != nil { httputils.WriteError(w, status.Errorf(httputils.RPCCode(sts), "%v", err)) } httputils.WriteResp(w, h) } // ConfigHistoryRevision implements the HistoryRevisionConfig RPC method. func (s *Service) ConfigHistoryRevision(w http.ResponseWriter, r *http.Request) { name := getName(r) rev, err := strconv.ParseInt(name, 10, 64) if err != nil { httputils.WriteError(w, status.Errorf(codes.InvalidArgument, "invalid history revision: %q (must be a positive integer)", name)) return } _, _, _, sts, err := s.handlerSetup(nil, r, noScope, nil) if err != nil { httputils.WriteError(w, status.Errorf(httputils.RPCCode(sts), "%v", err)) return } cfg := &pb.IcConfig{} if sts, err := s.realmReadTx(storage.ConfigDatatype, getRealm(r), storage.DefaultUser, storage.DefaultID, rev, cfg, nil); err != nil { httputils.WriteError(w, status.Errorf(httputils.RPCCode(sts), "%v", err)) return } httputils.WriteResp(w, cfg) } // ConfigReset implements the corresponding method in the IC API. func (s *Service) ConfigReset(w http.ResponseWriter, r *http.Request) { _, _, _, sts, err := s.handlerSetup(nil, r, noScope, nil) if err != nil { httputils.WriteError(w, status.Errorf(httputils.RPCCode(sts), "%v", err)) return } if _, err = s.store.Wipe(r.Context(), storage.AllRealms, 0, 0); err != nil { httputils.WriteError(w, status.Errorf(codes.Internal, "%v", err)) return } if err = ImportConfig(s.store, s.serviceName, nil, true, true, true); err != nil { httputils.WriteError(w, status.Errorf(codes.Internal, "%v", err)) return } // Reset clients in Hyrdra if s.useHydra { if getRealm(r) != storage.DefaultRealm { return } conf, err := s.loadConfig(nil, storage.DefaultRealm) if err != nil { httputils.WriteError(w, status.Errorf(codes.Unavailable, "%v", err)) return } secrets, err := s.loadSecrets(nil) if err != nil { httputils.WriteError(w, status.Errorf(codes.Unavailable, "%v", err)) return } if _, err := s.syncToHydra(conf.Clients, secrets.ClientSecrets, 0, nil); err != nil { httputils.WriteError(w, status.Errorf(codes.Unavailable, "%v", err)) return } } }