lib/dam/dam_integrity.go (687 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 dam import ( "context" "fmt" "net/http" "net/mail" "regexp" "sort" "strconv" "strings" "time" "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/adapter" /* copybara-comment: adapter */ "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/httputils" /* copybara-comment: httputils */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/oathclients" /* copybara-comment: oathclients */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/persona" /* copybara-comment: persona */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/scim" /* copybara-comment: scim */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/strutil" /* copybara-comment: strutil */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/validator" /* copybara-comment: validator */ cpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */ pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/dam/v1" /* copybara-comment: go_proto */ ) const ( cfgVisaTypes = "VisaTypes" cfgClients = "clients" cfgOptions = "options" cfgPolicies = "policies" cfgResources = "resources" cfgRoot = "cfg" cfgServiceTemplates = "serviceTemplates" cfgTestPersonas = "testPersonas" cfgTrustedPassportIssuer = "trustedPassportIssuer" cfgTrustedSources = "trustedSources" ) var ( interfaceRE = regexp.MustCompile(`\$\{(.*)\}`) ) // CheckIntegrity returns an error status if the config is invalid. func (s *Service) CheckIntegrity(cfg *pb.DamConfig, realm string, tx storage.Tx) *status.Status { return ValidateDAMConfig(cfg, s.ValidateCfgOpts(realm, tx)) } // ValidateCfgOpts returns the options for checking validity of configuration. func (s *Service) ValidateCfgOpts(realm string, tx storage.Tx) ValidateCfgOpts { return ValidateCfgOpts{ Services: s.adapters, DefaultBroker: s.defaultBroker, RoleCategories: s.roleCategories, HidePolicyBasis: s.hidePolicyBasis, HideRejectDetail: s.hideRejectDetail, Scim: s.scim, Realm: realm, Tx: tx, } } // ValidateCfgOpts contains options for ValidateDAMConfig. type ValidateCfgOpts struct { Services *adapter.ServiceAdapters DefaultBroker string RoleCategories map[string]*pb.RoleCategory HidePolicyBasis bool HideRejectDetail bool Scim *scim.Scim Realm string Tx storage.Tx } // ValidateDAMConfig checks that the provided config is valid. func ValidateDAMConfig(cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { if vopts.Services == nil { return httputils.NewStatus(codes.Unavailable, "services not loaded") } if st := checkBasicIntegrity(cfg, vopts); st != nil { return st } if st := checkExtraIntegrity(cfg, vopts); st != nil { return st } return nil } func checkBasicIntegrity(cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { for n, ti := range cfg.TrustedIssuers { if err := checkName(n); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, n), err.Error()) } if !httputils.IsHTTPS(ti.Issuer) && !httputils.IsLocalhost(ti.Issuer) { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, n, "issuer"), "trusted identity must have an issuer of type HTTPS") } if _, ok := translators[ti.TranslateUsing]; !ok && len(ti.TranslateUsing) > 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, n, "translateUsing"), fmt.Sprintf("trusted identity with unknown translator %q", ti.TranslateUsing)) } if path, err := check.CheckUI(ti.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, n, path), fmt.Sprintf("trusted passport issuer UI settings: %v", err)) } if stat := checkTrustedIssuerClientCredentials(n, vopts.DefaultBroker, ti, vopts); stat != nil { return stat } } for n, ts := range cfg.TrustedSources { if err := checkName(n); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedSources, n), err.Error()) } for i, source := range ts.Sources { if !httputils.IsHTTPS(source) { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedSources, n, "sources", strconv.Itoa(i)), "trusted source URL must be HTTPS") } } for i, visa := range ts.VisaTypes { if _, ok := cfg.VisaTypes[visa]; !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedSources, n, "visaTypes", strconv.Itoa(i)), fmt.Sprintf("visa name %q not found in visa type definitions", visa)) } } if path, err := check.CheckUI(ts.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedSources, n, path), fmt.Sprintf("trusted sources UI settings: %v", err)) } } for n, policy := range cfg.Policies { if err := checkName(n); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n), err.Error()) } if path, err := validator.ValidatePolicy(policy, cfg.VisaTypes, cfg.TrustedSources, nil); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n, path), err.Error()) } if path, err := check.CheckUI(policy.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n, path), fmt.Sprintf("policies UI settings: %v", err)) } // Note: there is no requirement that built-in policies be present. But if they are, they must not be edited. // Regular, non-built-in policies must not use reserved UI labels for built-in policies. builtin, ok := BuiltinPolicies[n] if ok && !proto.Equal(builtin, policy) { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n), fmt.Sprintf("built-in policy cannot be edited")) } if !ok && policy.Ui["source"] != "" { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n, "ui", "source"), fmt.Sprintf("%q label is reserved for built-in policies", "source")) } if !ok && policy.Ui["edit"] != "" { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgPolicies, n, "ui", "edit"), fmt.Sprintf("%q label is reserved for built-in policies", "edit")) } } for n, st := range cfg.ServiceTemplates { if stat := checkServiceTemplate(n, st, cfg, vopts); stat != nil { return stat } } for n, res := range cfg.Resources { if err := checkName(n); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, n), err.Error()) } for i, item := range res.Clients { if _, ok := cfg.Clients[item]; !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, n, "clients", strconv.Itoa(i)), fmt.Sprintf("client %q does not exist", item)) } } if len(res.MaxTokenTtl) > 0 && !ttlRE.Match([]byte(res.MaxTokenTtl)) { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, n, "maxTokenTtl"), "max token TTL invalid format") } for vn, view := range res.Views { if stat := checkViewIntegrity(vn, view, n, res, cfg, vopts); stat != nil { return stat } } if path, err := check.CheckUI(res.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, n, path), fmt.Sprintf("resource UI settings: %v", err)) } } for n, cl := range cfg.Clients { if err := oathclients.CheckClientIntegrity(n, cl); err != nil { return status.Convert(err) } } for n, def := range cfg.VisaTypes { if path, err := check.CheckUI(def.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgVisaTypes, n, path), fmt.Sprintf("claim definitions UI settings: %v", err)) } } personaEmail := make(map[string]string) for n, tp := range cfg.TestPersonas { if err := checkName(n); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n), err.Error()) } if tp.Passport == nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "passport"), "persona requires a passport") } tid, err := persona.ToIdentity(context.Background(), n, tp, defaultPersonaScope, "") if err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n), fmt.Sprintf("persona to identity: %v", err)) } if len(tid.Issuer) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "passport", "standardClaims", "iss"), "persona requires an issuer") } if len(tid.Subject) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "passport", "standardClaims", "sub"), "persona requires a subject") } if pmatch, ok := personaEmail[tid.Subject]; ok { return httputils.NewInfoStatus(codes.AlreadyExists, httputils.StatusPath(cfgTestPersonas, n, "passport", "standardClaims", "sub"), fmt.Sprintf("persona subject %q conflicts with test persona %q", tid.Subject, pmatch)) } for i, a := range tp.Passport.Ga4GhAssertions { // Test Persona conditions should meet the same criteria as policies that have no variables / arguments. policy := &pb.Policy{ AnyOf: a.AnyOfConditions, } if path, err := validator.ValidatePolicy(policy, cfg.VisaTypes, cfg.TrustedSources, nil); err != nil { path = strings.Replace(path, "anyOf/", "anyOfConditions/", 1) return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "passport", "ga4ghAssertions", strconv.Itoa(i), path), err.Error()) } } if path, err := check.CheckUI(tp.Ui, false); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, path), fmt.Sprintf("test persona UI settings: %v", err)) } // Checking persona expectations is in checkExtraIntegrity() to give an // opportunity for runTests() to catch problems and calculate a ConfigModification // response. } if stat := checkOptionsIntegrity(cfg.Options, vopts); stat != nil { return stat } if path, err := check.CheckUI(cfg.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgRoot, path), fmt.Sprintf("root config UI settings: %v", err)) } return nil } func checkExtraIntegrity(cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { for n, tp := range cfg.TestPersonas { for i, access := range tp.Access { aparts := strings.Split(access, "/") if len(aparts) != 3 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "access", strconv.Itoa(i)), "invalid access entry format (expecting 'resourceName/viewName/roleName')") } rn := aparts[0] vn := aparts[1] rolename := aparts[2] res, ok := cfg.Resources[rn] if !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "access", strconv.Itoa(i), "resource"), fmt.Sprintf("access entry resource %q not found", rn)) } view, ok := res.Views[vn] if !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "access", strconv.Itoa(i), "view"), fmt.Sprintf("access entry view %q not found", vn)) } roleView := makeView(vn, view, res, cfg, vopts.HidePolicyBasis, vopts.Services) if roleView.Roles == nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "access", strconv.Itoa(i), "role"), fmt.Sprintf("access entry no roles defined for view %q", vn)) } if _, ok := roleView.Roles[rolename]; !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, n, "access", strconv.Itoa(i), "role"), fmt.Sprintf("access entry role %q not found on view %q", rolename, vn)) } } } return nil } func checkViewIntegrity(name string, view *pb.View, resName string, res *pb.Resource, cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { if err := checkName(name); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name), err.Error()) } if len(view.ServiceTemplate) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "serviceTemplate"), "service template is not defined") } st, ok := cfg.ServiceTemplates[view.ServiceTemplate] if !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "serviceTemplate"), fmt.Sprintf("service template %q not found", view.ServiceTemplate)) } if len(view.Labels) == 0 || view.Labels["version"] == "" { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "metadata", "version"), "version is empty") } if path, err := checkAccessRequirements(view.ServiceTemplate, st, resName, name, view, cfg, vopts); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, path), fmt.Sprintf("access requirements: %v", err)) } if len(view.DefaultRole) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "defaultRole"), "default role is empty") } if _, ok := view.Roles[view.DefaultRole]; !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "defaultRole"), "default role is not defined within the view") } if len(view.ComputedInterfaces) > 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, "interfaces"), "interfaces should be determined at runtime and cannot be stored as part of the config") } if path, err := check.CheckUI(view.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgResources, resName, "views", name, path), fmt.Sprintf("view UI settings: %v", err)) } return nil } func checkServiceTemplate(name string, template *pb.ServiceTemplate, cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { if err := checkName(name); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgServiceTemplates, name), err.Error()) } if len(template.ServiceName) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgServiceTemplates, name, "serviceName"), "service is not specified") } service, ok := vopts.Services.ByServiceName[template.ServiceName] if !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgServiceTemplates, name, "serviceName", template.ServiceName), "service is not a recognized by this DAM") } if path, err := service.CheckConfig(name, template, "", "", nil, cfg, vopts.Services); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, path, err.Error()) } if path, err := checkServiceRoles(template.ServiceRoles, name, template.ServiceName, cfg, vopts); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, path, err.Error()) } varNames := make(map[string]bool) desc := vopts.Services.Descriptors[template.ServiceName] for varName, v := range desc.ItemVariables { varNames[varName] = true if v.Type != "const" && v.Type != "split_pattern" { return httputils.NewInfoStatus(codes.Internal, httputils.StatusPath("serviceDescriptors", template.ServiceName, "itemVariables", varName, "type"), fmt.Sprintf("variable type %q must be %q or %q", v.Type, "const", "split_pattern")) } } for k, v := range template.Interfaces { match := interfaceRE.FindAllString(v, -1) for _, varMatch := range match { // Remove the `${` prefix and `}` suffix. varName := varMatch[2 : len(varMatch)-1] if _, ok := varNames[varName]; !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgServiceTemplates, name, "interfaces", k), fmt.Sprintf("interface %q variable %q not defined for this service", k, varName)) } } } if path, err := check.CheckUI(template.Ui, true); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgServiceTemplates, name, path), fmt.Sprintf("service template UI settings: %v", err)) } return nil } func checkAccessRequirements(templateName string, template *pb.ServiceTemplate, resName, viewName string, view *pb.View, cfg *pb.DamConfig, vopts ValidateCfgOpts) (string, error) { adapt, ok := vopts.Services.ByServiceName[template.ServiceName] if !ok { return httputils.StatusPath("services"), fmt.Errorf("service template %q service %q is not a recognized by this DAM", templateName, template.ServiceName) } if path, err := adapt.CheckConfig(templateName, template, resName, viewName, view, cfg, vopts.Services); err != nil { return path, err } if path, err := checkAccessRoles(view.Roles, templateName, template.ServiceName, cfg, vopts); err != nil { return httputils.StatusPath("views", viewName, "roles", path), fmt.Errorf("invalid view: %v", err) } desc, ok := vopts.Services.Descriptors[template.ServiceName] if !ok { return httputils.StatusPath("services", template.ServiceName), fmt.Errorf("internal error: service %q does not have a service descriptor", template.ServiceName) } if len(desc.ItemVariables) > 0 && len(view.Items) == 0 { return httputils.StatusPath("views", viewName, "items"), fmt.Errorf("view %q does not provide any target items", viewName) } if len(desc.ItemVariables) > 0 && desc.Properties != nil && desc.Properties.SingleItem && len(view.Items) > 1 { return httputils.StatusPath("views", viewName, "items"), fmt.Errorf("view %q provides more than one item when only one was expected for service %q", viewName, template.ServiceName) } for idx, item := range view.Items { vars, path, err := adapter.GetItemVariables(vopts.Services, template.ServiceName, item) if err != nil { return httputils.StatusPath("views", viewName, "items", strconv.Itoa(idx), path), err } if len(vars) == 0 { return httputils.StatusPath("views", viewName, "items", strconv.Itoa(idx), "vars"), fmt.Errorf("no variables defined") } } return "", nil } func checkAccessRoles(roles map[string]*pb.ViewRole, templateName, serviceName string, cfg *pb.DamConfig, vopts ValidateCfgOpts) (string, error) { if len(roles) == 0 { return "", fmt.Errorf("a view must have at least one role with a selected policy") } desc := vopts.Services.Descriptors[serviceName] for rname, role := range roles { if err := checkName(rname); err != nil { return httputils.StatusPath(rname), fmt.Errorf("role has invalid name %q: %v", rname, err) } if len(role.ComputedPolicyBasis) > 0 { return httputils.StatusPath(rname, "roleCategories"), fmt.Errorf("role %q roleCategories should be determined at runtime and cannot be stored as part of the config", rname) } if len(role.ComputedPolicyBasis) > 0 { return httputils.StatusPath(rname, "policyBasis"), fmt.Errorf("role %q policyBasis should be determined at runtime and cannot be stored as part of the config", rname) } if len(role.Policies) > 20 { return httputils.StatusPath(rname, "policies"), fmt.Errorf("role exceeeds policy limit") } hasAllowlist := false for i, p := range role.Policies { if len(p.Name) == 0 { return httputils.StatusPath(rname, "policies", strconv.Itoa(i), "name"), fmt.Errorf("access policy name is not defined") } if p.Name == allowlistPolicyName { hasAllowlist = true emails := strings.Split(p.Args["users"], ";") if len(emails) > 20 { return httputils.StatusPath(rname, "policies", strconv.Itoa(i), "args", "users"), fmt.Errorf("number of emails on allowlist policy exceeeds limit") } for j, email := range emails { if _, err := mail.ParseAddress(email); err != nil { return httputils.StatusPath(rname, "policies", strconv.Itoa(i), "args", "users"), fmt.Errorf("email entry %d (%q) is invalid", j, email) } } } policy, ok := cfg.Policies[p.Name] if !ok { return httputils.StatusPath(rname, "policies", strconv.Itoa(i), "name"), fmt.Errorf("policy %q is not defined", p.Name) } if path, err := validator.ValidatePolicy(policy, cfg.VisaTypes, cfg.TrustedSources, p.Args); err != nil { return httputils.StatusPath(rname, "policies", strconv.Itoa(i), path), err } } if len(role.Policies) == 0 && !desc.Properties.IsAggregate { return httputils.StatusPath(rname, "policies"), fmt.Errorf("must provide at least one target policy") } if hasAllowlist && len(role.Policies) > 1 { return httputils.StatusPath(rname, "policies"), fmt.Errorf("allowlist policies cannot be used in combination with any other policies") } } return "", nil } func checkServiceRoles(roles map[string]*pb.ServiceRole, templateName, serviceName string, cfg *pb.DamConfig, vopts ValidateCfgOpts) (string, error) { if len(roles) == 0 { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles"), fmt.Errorf("no roles provided") } desc := vopts.Services.Descriptors[serviceName] for rname, role := range roles { if err := checkName(rname); err != nil { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname), fmt.Errorf("role has invalid name %q: %v", rname, err) } if len(role.DamRoleCategories) == 0 { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, "damRoleCategories"), fmt.Errorf("role %q does not provide a DAM role category", rname) } for i, pt := range role.DamRoleCategories { if _, ok := vopts.RoleCategories[pt]; !ok { return httputils.StatusPath( cfgServiceTemplates, templateName, "roles", rname, "damRoleCategories", strconv.Itoa(i)), fmt.Errorf("role %q DAM role category %q is not defined (valid types are: %s)", rname, pt, strings.Join(roleCategorySet(vopts.RoleCategories), ", ")) } } for vname, def := range desc.ServiceVariables { arg, ok := role.ServiceArgs[vname] if !ok { if def.Optional { continue } return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, "serviceArgs", vname), fmt.Errorf("missing required service argument %q", vname) } re, err := regexp.Compile(def.Regexp) if err != nil { return httputils.StatusPath("services", templateName, "serviceArgs", vname), fmt.Errorf("variable format regexp %q is not a valid regular expression", def.Regexp) } for ival, val := range arg.Values { if len(val) == 0 { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, "serviceArgs", vname, "values", strconv.Itoa(ival)), fmt.Errorf("service argument value %d is empty", ival) } if !re.MatchString(val) { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, "serviceArgs", vname, "values", strconv.Itoa(ival)), fmt.Errorf("service argument value %q is not valid", val) } } } for aname := range role.ServiceArgs { if _, ok := desc.ServiceVariables[aname]; !ok { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, "serviceArgs", aname), fmt.Errorf("service argument name %q is not a known input for service %q", aname, serviceName) } } if path, err := check.CheckUI(role.Ui, true); err != nil { return httputils.StatusPath(cfgServiceTemplates, templateName, "roles", rname, path), fmt.Errorf("role %q: %v", rname, err) } } return "", nil } func checkOptionsIntegrity(opts *pb.ConfigOptions, vopts ValidateCfgOpts) *status.Status { if opts == nil { return nil } // Get the descriptors. opts = makeConfigOptions(opts) if err := check.CheckIntOption(opts.AwsManagedKeysPerIamUser, "awsManagedKeysPerIamUser", opts.ComputedDescriptors); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgOptions, "awsManagedKeysPerIamUser"), err.Error()) } if err := check.CheckStringOption(opts.GcpManagedKeysMaxRequestedTtl, "gcpManagedKeysMaxRequestedTtl", opts.ComputedDescriptors); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgOptions, "gcpManagedKeysMaxRequestedTtl"), err.Error()) } if err := check.CheckIntOption(opts.GcpManagedKeysPerAccount, "gcpManagedKeysPerAccount", opts.ComputedDescriptors); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgOptions, "gcpManagedKeysPerAccount"), err.Error()) } if err := check.CheckStringOption(opts.GcpServiceAccountProject, "gcpServiceAccountProject", opts.ComputedDescriptors); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgOptions, "gcpServiceAccountProject"), err.Error()) } if err := check.CheckStringOption(opts.GcpIamBillingProject, "gcpIamBillingProject", opts.ComputedDescriptors); err != nil { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgOptions, "gcpIamBillingProject"), err.Error()) } return nil } func configCheckIntegrity(cfg *pb.DamConfig, mod *pb.ConfigModification, r *http.Request, vopts ValidateCfgOpts) *status.Status { bad := codes.InvalidArgument if err := check.ValidToWriteConfig(getRealm(r), cfg.Options.ReadOnlyMasterRealm); err != nil { return httputils.NewStatus(bad, err.Error()) } if len(cfg.Version) == 0 { return httputils.NewStatus(bad, "missing config version") } if cfg.Revision <= 0 { return httputils.NewStatus(bad, "invalid config revision") } if err := configRevision(mod, cfg); err != nil { return httputils.NewStatus(bad, err.Error()) } if stat := updateTests(cfg, mod, vopts); stat != nil { return stat } if stat := checkBasicIntegrity(cfg, vopts); stat != nil { return stat } if tests := runTests(r.Context(), cfg, nil, vopts); hasTestError(tests) { stat := httputils.NewStatus(codes.FailedPrecondition, tests.Error) return httputils.AddStatusDetails(stat, tests.Modification) } if stat := checkExtraIntegrity(cfg, vopts); stat != nil { return stat } return nil } func checkTrustedIssuerClientCredentials(name, defaultBroker string, tpi *pb.TrustedIssuer, vopts ValidateCfgOpts) *status.Status { if name != defaultBroker { return nil } if len(tpi.AuthUrl) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, name, "authUrl"), "AuthUrl not provided") } if len(tpi.TokenUrl) == 0 { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTrustedPassportIssuer, name, "tokenUrl"), "TokenUrl not provided") } return nil } func checkTrustedIssuer(iss string, cfg *pb.DamConfig, vopts ValidateCfgOpts) *status.Status { if len(iss) == 0 { return httputils.NewStatus(codes.PermissionDenied, "unauthorized missing passport issuer") } foundIssuer := false for _, ti := range cfg.TrustedIssuers { if iss == ti.Issuer { foundIssuer = true break } } if !foundIssuer { return httputils.NewStatus(codes.PermissionDenied, fmt.Sprintf("unauthorized passport issuer %q", iss)) } return nil } func rmTestResource(cfg *pb.DamConfig, name string) { prefix := name + "/" for _, p := range cfg.TestPersonas { p.Access = strutil.FilterStringsByPrefix(p.Access, prefix) } } func rmTestView(cfg *pb.DamConfig, resName, viewName string) { prefix := resName + "/" + viewName + "/" for _, p := range cfg.TestPersonas { p.Access = strutil.FilterStringsByPrefix(p.Access, prefix) } } func updateTests(cfg *pb.DamConfig, modification *pb.ConfigModification, vopts ValidateCfgOpts) *status.Status { if modification == nil { return nil } for name, td := range modification.TestPersonas { p, ok := cfg.TestPersonas[name] if !ok { return httputils.NewInfoStatus(codes.InvalidArgument, httputils.StatusPath(cfgTestPersonas, name), fmt.Sprintf("test persona %q not found", name)) } p.Access = td.Access sort.Strings(p.Access) } return nil } func runTests(ctx context.Context, cfg *pb.DamConfig, resources []string, vopts ValidateCfgOpts) *pb.GetTestResultsResponse { t := float64(time.Now().UnixNano()) / 1e9 personas := make(map[string]*cpb.TestPersona) results := make([]*pb.GetTestResultsResponse_TestResult, 0) passed := int32(0) tc := int32(0) if resources == nil { resources = make([]string, 0, len(cfg.Resources)) for k := range cfg.Resources { resources = append(resources, k) } } modification := &pb.ConfigModification{ TestPersonas: make(map[string]*pb.ConfigModification_PersonaModification), } for pname, p := range cfg.TestPersonas { tc++ personas[pname] = &cpb.TestPersona{ Passport: p.Passport, Access: p.Access, } status, got, rejectedVisas, err := testPersona(ctx, pname, resources, cfg, vopts) e := "" if err == nil { passed++ } else { e = err.Error() } results = append(results, &pb.GetTestResultsResponse_TestResult{ Name: pname, Result: status, Access: got, RejectedVisas: makeRejectedVisas(rejectedVisas), Error: e, }) calculateModification(pname, p.Access, got, modification, vopts) } e := "" if passed < tc { e = fmt.Errorf("%d of %d tests passed, %d failed", passed, tc, tc-passed).Error() } return &pb.GetTestResultsResponse{ Version: cfg.Version, Revision: cfg.Revision, Timestamp: t, Personas: personas, TestResults: results, Executed: tc, Passed: passed, Modification: modification, Error: e, } } func makeRejectedVisas(rejected []*ga4gh.RejectedVisa) []*pb.GetTestResultsResponse_RejectedVisa { if len(rejected) == 0 { return nil } out := []*pb.GetTestResultsResponse_RejectedVisa{} for _, reject := range rejected { out = append(out, &pb.GetTestResultsResponse_RejectedVisa{ Reason: reject.Rejection.Reason, Field: reject.Rejection.Field, Description: reject.Rejection.Description, VisaType: string(reject.Assertion.Type), Source: string(reject.Assertion.Source), Value: string(reject.Assertion.Value), By: string(reject.Assertion.By), }) } return out } func hasTestError(tr *pb.GetTestResultsResponse) bool { return len(tr.Error) > 0 } func calculateModification(name string, want []string, got []string, modification *pb.ConfigModification, vopts ValidateCfgOpts) { entry, ok := modification.TestPersonas[name] if !ok { entry = &pb.ConfigModification_PersonaModification{ Access: got, AddAccess: []string{}, RemoveAccess: []string{}, } modification.TestPersonas[name] = entry } deltaResourceModification(entry, want, got, vopts) if len(entry.AddAccess) == 0 && len(entry.RemoveAccess) == 0 { delete(modification.TestPersonas, name) } } func deltaResourceModification(entry *pb.ConfigModification_PersonaModification, want []string, got []string, vopts ValidateCfgOpts) bool { // Assumes view list entries are sorted on both |want| and |got|. var add []string var rm []string w := 0 g := 0 wl := 0 if want != nil { wl = len(want) } gl := 0 if got != nil { gl = len(got) } for w < wl || g < gl { if w >= wl { add = append(add, got[g:]...) break } if g >= gl { rm = append(rm, want[w:]...) break } if c := strings.Compare(want[w], got[g]); c == 0 { w++ g++ } else if c < 0 { rm = append(rm, want[w]) w++ } else { add = append(add, got[g]) g++ } } if len(add) == 0 && len(rm) == 0 { return false } if len(add) > 0 { entry.AddAccess = append(entry.AddAccess, add...) } if len(rm) > 0 { entry.RemoveAccess = append(entry.RemoveAccess, rm...) } return true }