github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/open_resource_discovery/validation.go (about) 1 package ord 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "regexp" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/mitchellh/hashstructure/v2" 12 13 "golang.org/x/mod/semver" 14 15 "github.com/google/go-cmp/cmp" 16 "github.com/kyma-incubator/compass/components/director/pkg/str" 17 18 validation "github.com/go-ozzo/ozzo-validation/v4" 19 "github.com/go-ozzo/ozzo-validation/v4/is" 20 "github.com/kyma-incubator/compass/components/director/internal/model" 21 22 "github.com/pkg/errors" 23 "github.com/tidwall/gjson" 24 ) 25 26 // Disclaimer: All regexes below are provided by the ORD spec itself. 27 const ( 28 // SemVerRegex represents the valid structure of the field 29 SemVerRegex = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" 30 // PackageOrdIDRegex represents the valid structure of the ordID of the Package 31 PackageOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(package):([a-zA-Z0-9._\\-]+):(alpha|beta|v[0-9]+)$" 32 // VendorOrdIDRegex represents the valid structure of the ordID of the Vendor 33 VendorOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(vendor):([a-zA-Z0-9._\\-]+):()$" 34 // ProductOrdIDRegex represents the valid structure of the ordID of the Product 35 ProductOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(product):([a-zA-Z0-9._\\-]+):()$" 36 // BundleOrdIDRegex represents the valid structure of the ordID of the ConsumptionBundle 37 BundleOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(consumptionBundle):([a-zA-Z0-9._\\-]+):(alpha|beta|v[0-9]+)$" 38 // TombstoneOrdIDRegex represents the valid structure of the ordID of the Tombstone 39 TombstoneOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(package|consumptionBundle|product|vendor|apiResource|eventResource):([a-zA-Z0-9._\\-]+):(alpha|beta|v[0-9]+|)$" 40 // SystemInstanceBaseURLRegex represents the valid structure of the field 41 SystemInstanceBaseURLRegex = "^http[s]?:\\/\\/[^:\\/\\s]+\\.[^:\\/\\s\\.]+(:\\d+)?(\\/[a-zA-Z0-9-\\._~]+)?$" 42 // ConfigBaseURLRegex represents the valid structure of the field 43 ConfigBaseURLRegex = "^http[s]?:\\/\\/[^:\\/\\s]+\\.[^:\\/\\s\\.]+(:\\d+)?(\\/[a-zA-Z0-9-\\._~]+)?$" 44 // StringArrayElementRegex represents the valid structure of the field 45 StringArrayElementRegex = "^[a-zA-Z0-9-_.\\/ ]*$" 46 // CountryRegex represents the valid structure of the field 47 CountryRegex = "^[A-Z]{2}$" 48 // APIOrdIDRegex represents the valid structure of the ordID of the API 49 APIOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(apiResource):([a-zA-Z0-9._\\-]+):(alpha|beta|v[0-9]+)$" 50 // EventOrdIDRegex represents the valid structure of the ordID of the Event 51 EventOrdIDRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(eventResource):([a-zA-Z0-9._\\-]+):(alpha|beta|v[0-9]+)$" 52 // CorrelationIDsRegex represents the valid structure of the field 53 CorrelationIDsRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):([a-zA-Z0-9._\\-\\/]+):([a-zA-Z0-9._\\-\\/]+)$" 54 // LabelsKeyRegex represents the valid structure of the field 55 LabelsKeyRegex = "^[a-zA-Z0-9-_.]*$" 56 // NoNewLineRegex represents the valid structure of the field 57 NoNewLineRegex = "^[^\\n]*$" 58 // CustomImplementationStandardRegex represents the valid structure of the field 59 CustomImplementationStandardRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):([a-zA-Z0-9._\\-]+):v([0-9]+)$" 60 // VendorPartnersRegex represents the valid structure of the field 61 VendorPartnersRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):(vendor):([a-zA-Z0-9._\\-]+):()$" 62 // CustomPolicyLevelRegex represents the valid structure of the field 63 CustomPolicyLevelRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):([a-zA-Z0-9._\\-]+):v([0-9]+)$" 64 // CustomTypeCredentialExchangeStrategyRegex represents the valid structure of the field 65 CustomTypeCredentialExchangeStrategyRegex = "^([a-z0-9-]+(?:[.][a-z0-9-]+)*):([a-zA-Z0-9._\\-]+):v([0-9]+)$" 66 // SAPProductOrdIDNamespaceRegex represents the valid structure of a SAP Product OrdID Namespace part 67 SAPProductOrdIDNamespaceRegex = "^(sap)((\\.)([a-z0-9-]+(?:[.][a-z0-9-]+)*))*$" 68 // OrdNamespaceRegex represents the valid structure of a Ord Namespace 69 OrdNamespaceRegex = "^[a-z0-9]+(?:[.][a-z0-9]+)*$" 70 71 // MinDescriptionLength represents the minimal accepted length of the Description field 72 MinDescriptionLength = 1 73 // MaxDescriptionLength represents the minimal accepted length of the Description field 74 MaxDescriptionLength = 5000 75 // MinLocalTenantIDLength represents the minimal accepted length of the LocalID field 76 MinLocalTenantIDLength = 1 77 // MaxLocalTenantIDLength represents the minimal accepted length of the LocalID field 78 MaxLocalTenantIDLength = 255 79 // MinSystemVersionTitleLength represents the minimal accepted length of the LocalID field 80 MinSystemVersionTitleLength = 1 81 // MaxSystemVersionTitleLength represents the minimal accepted length of the LocalID field 82 MaxSystemVersionTitleLength = 255 83 ) 84 85 const ( 86 custom string = "custom" 87 none string = "none" 88 89 // PolicyLevelSap is one of the available policy options 90 PolicyLevelSap string = "sap:core:v1" 91 // PolicyLevelSapPartner is one of the available policy options 92 PolicyLevelSapPartner string = "sap:partner:v1" 93 // PolicyLevelCustom is one of the available policy options 94 PolicyLevelCustom = custom 95 // PolicyLevelNone is one of the available policy options 96 PolicyLevelNone string = none 97 98 // ReleaseStatusBeta is one of the available release status options 99 ReleaseStatusBeta string = "beta" 100 // ReleaseStatusActive is one of the available release status options 101 ReleaseStatusActive string = "active" 102 // ReleaseStatusDeprecated is one of the available release status options 103 ReleaseStatusDeprecated string = "deprecated" 104 105 // APIProtocolODataV2 is one of the available api protocol options 106 APIProtocolODataV2 string = "odata-v2" 107 // APIProtocolODataV4 is one of the available api protocol options 108 APIProtocolODataV4 string = "odata-v4" 109 // APIProtocolSoapInbound is one of the available api protocol options 110 APIProtocolSoapInbound string = "soap-inbound" 111 // APIProtocolSoapOutbound is one of the available api protocol options 112 APIProtocolSoapOutbound string = "soap-outbound" 113 // APIProtocolRest is one of the available api protocol options 114 APIProtocolRest string = "rest" 115 // APIProtocolSapRfc is one of the available api protocol options 116 APIProtocolSapRfc string = "sap-rfc" 117 // APIProtocolWebsocket is one of the available api protocol options 118 APIProtocolWebsocket string = "websocket" 119 // APIProtocolSAPSQLAPIV1 is one of the available api protocol options 120 APIProtocolSAPSQLAPIV1 string = "sap-sql-api-v1" 121 122 // APIVisibilityPublic is one of the available api visibility options 123 APIVisibilityPublic string = "public" 124 // APIVisibilityPrivate is one of the available api visibility options 125 APIVisibilityPrivate string = "private" 126 // APIVisibilityInternal is one of the available api visibility options 127 APIVisibilityInternal string = "internal" 128 129 // APIImplementationStandardDocumentAPI is one of the available api implementation standard options 130 APIImplementationStandardDocumentAPI string = "sap:ord-document-api:v1" 131 // APIImplementationStandardServiceBroker is one of the available api implementation standard options 132 APIImplementationStandardServiceBroker string = "cff:open-service-broker:v2" 133 // APIImplementationStandardCsnExposure is one of the available api implementation standard options 134 APIImplementationStandardCsnExposure string = "sap:csn-exposure:v1" 135 // APIImplementationStandardApeAPI is one of the available api implementation standard options 136 APIImplementationStandardApeAPI string = "sap:ape-api:v1" 137 // APIImplementationStandardCdiAPI is one of the available api implementation standard options 138 APIImplementationStandardCdiAPI string = "sap:cdi-api:v1" 139 // APIImplementationStandardCustom is one of the available api implementation standard options 140 APIImplementationStandardCustom = custom 141 142 // SapVendor is a valid Vendor ordID 143 SapVendor = "sap:vendor:SAP:" 144 // PartnerVendor is a valid partner Vendor ordID 145 PartnerVendor = "partner:vendor:SAP:" 146 ) 147 148 var ( 149 // LineOfBusinesses contain all valid values for this field from the spec 150 LineOfBusinesses = map[string]bool{ 151 "Asset Management": true, 152 "Commerce": true, 153 "Finance": true, 154 "Human Resources": true, 155 "Manufacturing": true, 156 "Marketing": true, 157 "R&D Engineering": true, 158 "Sales": true, 159 "Service": true, 160 "Sourcing and Procurement": true, 161 "Supply Chain": true, 162 "Sustainability": true, 163 "Metering": true, 164 "Grid Operations and Maintenance": true, 165 "Plant Operations and Maintenance": true, 166 "Maintenance and Engineering": true, 167 } 168 // Industries contain all valid values for this field from the spec 169 Industries = map[string]bool{ 170 "Aerospace and Defense": true, 171 "Automotive": true, 172 "Banking": true, 173 "Chemicals": true, 174 "Consumer Products": true, 175 "Defense and Security": true, 176 "Engineering Construction and Operations": true, 177 "Healthcare": true, 178 "Higher Education and Research": true, 179 "High Tech": true, 180 "Industrial Machinery and Components": true, 181 "Insurance": true, 182 "Life Sciences": true, 183 "Media": true, 184 "Mill Products": true, 185 "Mining": true, 186 "Oil and Gas": true, 187 "Professional Services": true, 188 "Public Sector": true, 189 "Retail": true, 190 "Sports and Entertainment": true, 191 "Telecommunications": true, 192 "Travel and Transportation": true, 193 "Utilities": true, 194 "Wholesale Distribution": true, 195 } 196 // SupportedUseCases contain all valid values for this field from the spec 197 SupportedUseCases = map[string]bool{ 198 "mass-extraction": true, 199 // "mass-import": true, // will be added later in spec 200 } 201 ) 202 203 var shortDescriptionRules = []validation.Rule{ 204 validation.Required, validation.RuneLength(1, 256), validation.NewStringRule(noNewLines, "short description should not contain line breaks"), 205 } 206 207 var optionalShortDescriptionRules = []validation.Rule{ 208 validation.NilOrNotEmpty, validation.RuneLength(1, 256), validation.NewStringRule(noNewLines, "short description should not contain line breaks"), 209 } 210 211 // ORDDocumentValidationError contains the validation errors when aggregating ord documents 212 type ORDDocumentValidationError struct { 213 Err error 214 } 215 216 func (e *ORDDocumentValidationError) Error() string { 217 return e.Err.Error() 218 } 219 220 // ValidateSystemInstanceInput validates the given SystemInstance 221 func ValidateSystemInstanceInput(app *model.Application) error { 222 return validation.ValidateStruct(app, 223 validation.Field(&app.CorrelationIDs, validation.By(func(value interface{}) error { 224 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CorrelationIDsRegex)) 225 })), 226 validation.Field(&app.LocalTenantID, validation.NilOrNotEmpty, validation.Length(MinLocalTenantIDLength, MaxLocalTenantIDLength)), 227 validation.Field(&app.ApplicationNamespace, validation.Match(regexp.MustCompile(OrdNamespaceRegex))), 228 validation.Field(&app.BaseURL, is.RequestURI, validation.Match(regexp.MustCompile(SystemInstanceBaseURLRegex))), 229 validation.Field(&app.OrdLabels, validation.By(validateORDLabels)), 230 validation.Field(&app.Tags, validation.By(func(value interface{}) error { 231 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 232 })), 233 validation.Field(&app.DocumentationLabels, validation.By(validateDocumentationLabels)), 234 ) 235 } 236 237 // ValidateSystemVersionInput validates the given SystemVersion 238 func ValidateSystemVersionInput(appTemplateVersion *model.ApplicationTemplateVersionInput) error { 239 return validation.ValidateStruct(appTemplateVersion, 240 validation.Field(&appTemplateVersion.Title, validation.NilOrNotEmpty, validation.Length(MinSystemVersionTitleLength, MaxSystemVersionTitleLength), validation.Match(regexp.MustCompile(NoNewLineRegex))), 241 validation.Field(&appTemplateVersion.ReleaseDate, validation.Required), 242 ) 243 } 244 245 func validateDocumentInput(doc *Document) error { 246 return validation.ValidateStruct(doc, validation.Field(&doc.OpenResourceDiscovery, validation.Required, validation.Match(regexp.MustCompile("^1.*$")))) 247 } 248 249 func validatePackageInput(pkg *model.PackageInput, packagesFromDB map[string]*model.Package, resourceHashes map[string]uint64) error { 250 return validation.ValidateStruct(pkg, 251 validation.Field(&pkg.OrdID, validation.Required, validation.Match(regexp.MustCompile(PackageOrdIDRegex))), 252 validation.Field(&pkg.Title, validation.Length(1, 255), validation.Required), 253 validation.Field(&pkg.ShortDescription, shortDescriptionRules...), 254 validation.Field(&pkg.Description, validation.Required, validation.Length(MinDescriptionLength, MaxDescriptionLength)), 255 validation.Field(&pkg.SupportInfo, validation.NilOrNotEmpty), 256 validation.Field(&pkg.Version, validation.Required, validation.Match(regexp.MustCompile(SemVerRegex)), validation.By(func(value interface{}) error { 257 return validatePackageVersionInput(value, *pkg, packagesFromDB, resourceHashes) 258 })), 259 validation.Field(&pkg.PolicyLevel, validation.Required, validation.In(PolicyLevelSap, PolicyLevelSapPartner, PolicyLevelCustom, PolicyLevelNone), validation.When(pkg.CustomPolicyLevel != nil, validation.In(PolicyLevelCustom))), 260 validation.Field(&pkg.CustomPolicyLevel, validation.When(pkg.PolicyLevel != PolicyLevelCustom, validation.Empty), validation.Match(regexp.MustCompile(CustomPolicyLevelRegex))), 261 validation.Field(&pkg.PackageLinks, validation.By(validatePackageLinks)), 262 validation.Field(&pkg.Links, validation.By(validateORDLinks)), 263 validation.Field(&pkg.Vendor, validation.Required, validation.When(pkg.PolicyLevel == PolicyLevelSap, validation.In(SapVendor)), validation.When(pkg.PolicyLevel == PolicyLevelSapPartner, validation.NotIn(SapVendor)), validation.Match(regexp.MustCompile(VendorOrdIDRegex))), 264 validation.Field(&pkg.PartOfProducts, validation.Required, validation.By(func(value interface{}) error { 265 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(ProductOrdIDRegex)) 266 })), 267 validation.Field(&pkg.Tags, validation.By(func(value interface{}) error { 268 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 269 })), 270 validation.Field(&pkg.Labels, validation.By(validateORDLabels)), 271 validation.Field(&pkg.Countries, validation.By(func(value interface{}) error { 272 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CountryRegex)) 273 })), 274 validation.Field(&pkg.LineOfBusiness, 275 validation.By(func(value interface{}) error { 276 if pkg.PolicyLevel != PolicyLevelSap { 277 return nil 278 } 279 return validateJSONArrayOfStringsContainsInMap(value, LineOfBusinesses) 280 }), 281 validation.By(func(value interface{}) error { 282 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 283 }), 284 ), 285 validation.Field(&pkg.Industry, 286 validation.By(func(value interface{}) error { 287 if pkg.PolicyLevel != PolicyLevelSap { 288 return nil 289 } 290 return validateJSONArrayOfStringsContainsInMap(value, Industries) 291 }), 292 validation.By(func(value interface{}) error { 293 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 294 }), 295 ), 296 validation.Field(&pkg.DocumentationLabels, validation.By(validateDocumentationLabels)), 297 ) 298 } 299 300 func validateBundleInput(bndl *model.BundleCreateInput, bundlesFromDB map[string]*model.Bundle, resourceHashes map[string]uint64, credentialExchangeStrategyTenantMappings map[string]CredentialExchangeStrategyTenantMapping) error { 301 return validation.ValidateStruct(bndl, 302 validation.Field(&bndl.OrdID, validation.Required, validation.Match(regexp.MustCompile(BundleOrdIDRegex))), 303 validation.Field(&bndl.LocalTenantID, validation.NilOrNotEmpty, validation.Length(MinLocalTenantIDLength, MaxLocalTenantIDLength)), 304 validation.Field(&bndl.Name, validation.Required), 305 validation.Field(&bndl.ShortDescription, optionalShortDescriptionRules...), 306 validation.Field(&bndl.Description, validation.NilOrNotEmpty, validation.Length(MinDescriptionLength, MaxDescriptionLength)), 307 validation.Field(&bndl.Version, validation.Match(regexp.MustCompile(SemVerRegex)), validation.By(func(value interface{}) error { 308 return validateBundleVersionInput(value, *bndl, bundlesFromDB, resourceHashes) 309 })), 310 validation.Field(&bndl.Links, validation.By(validateORDLinks)), 311 validation.Field(&bndl.Labels, validation.By(validateORDLabels)), 312 validation.Field(&bndl.CredentialExchangeStrategies, validation.By(func(value interface{}) error { 313 return validateJSONArrayOfObjects(value, map[string][]validation.Rule{ 314 "type": { 315 validation.Required, 316 validation.In(custom), 317 }, 318 "callbackUrl": { 319 is.RequestURI, 320 }, 321 }, validateCustomType(credentialExchangeStrategyTenantMappings), validateCustomDescription) 322 })), 323 validation.Field(&bndl.CorrelationIDs, validation.By(func(value interface{}) error { 324 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CorrelationIDsRegex)) 325 })), 326 validation.Field(&bndl.Tags, validation.By(func(value interface{}) error { 327 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 328 })), 329 validation.Field(&bndl.DocumentationLabels, validation.By(validateDocumentationLabels)), 330 ) 331 } 332 333 func validateAPIInput(api *model.APIDefinitionInput, packagePolicyLevels map[string]string, apisFromDB map[string]*model.APIDefinition, apiHashes map[string]uint64) error { 334 return validation.ValidateStruct(api, 335 validation.Field(&api.OrdID, validation.Required, validation.Match(regexp.MustCompile(APIOrdIDRegex))), 336 validation.Field(&api.LocalTenantID, validation.NilOrNotEmpty, validation.Length(MinLocalTenantIDLength, MaxLocalTenantIDLength)), 337 validation.Field(&api.Name, validation.Required), 338 validation.Field(&api.ShortDescription, shortDescriptionRules...), 339 validation.Field(&api.Description, validation.Required, validation.Length(MinDescriptionLength, MaxDescriptionLength)), 340 validation.Field(&api.PolicyLevel, validation.In(PolicyLevelSap, PolicyLevelSapPartner, PolicyLevelCustom, PolicyLevelNone), validation.When(api.CustomPolicyLevel != nil, validation.In(PolicyLevelCustom))), 341 validation.Field(&api.CustomPolicyLevel, validation.When(api.PolicyLevel != nil && *api.PolicyLevel != PolicyLevelCustom, validation.Empty), validation.Match(regexp.MustCompile(CustomPolicyLevelRegex))), 342 validation.Field(&api.VersionInput.Value, validation.Required, validation.Match(regexp.MustCompile(SemVerRegex)), validation.By(func(value interface{}) error { 343 return validateAPIDefinitionVersionInput(value, *api, apisFromDB, apiHashes) 344 })), 345 validation.Field(&api.OrdPackageID, validation.Required, validation.Match(regexp.MustCompile(PackageOrdIDRegex))), 346 validation.Field(&api.APIProtocol, validation.Required, validation.In(APIProtocolODataV2, APIProtocolODataV4, APIProtocolSoapInbound, APIProtocolSoapOutbound, APIProtocolRest, APIProtocolSapRfc, APIProtocolWebsocket, APIProtocolSAPSQLAPIV1)), 347 validation.Field(&api.Visibility, validation.Required, validation.In(APIVisibilityPublic, APIVisibilityInternal, APIVisibilityPrivate)), 348 validation.Field(&api.PartOfProducts, validation.By(func(value interface{}) error { 349 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(ProductOrdIDRegex)) 350 })), 351 validation.Field(&api.SupportedUseCases, 352 validation.By(func(value interface{}) error { 353 return validateJSONArrayOfStringsContainsInMap(value, SupportedUseCases) 354 }), 355 ), 356 validation.Field(&api.Hierarchy, validation.By(func(value interface{}) error { 357 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 358 })), 359 validation.Field(&api.Tags, validation.By(func(value interface{}) error { 360 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 361 })), 362 validation.Field(&api.Countries, validation.By(func(value interface{}) error { 363 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CountryRegex)) 364 })), 365 validation.Field(&api.LineOfBusiness, 366 validation.By(func(value interface{}) error { 367 return validateWhenPolicyLevelIsSAP(api.OrdPackageID, packagePolicyLevels, func() error { 368 return validateJSONArrayOfStringsContainsInMap(value, LineOfBusinesses) 369 }) 370 }), 371 validation.By(func(value interface{}) error { 372 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 373 }), 374 ), 375 validation.Field(&api.Industry, 376 validation.By(func(value interface{}) error { 377 return validateWhenPolicyLevelIsSAP(api.OrdPackageID, packagePolicyLevels, func() error { 378 return validateJSONArrayOfStringsContainsInMap(value, Industries) 379 }) 380 }), 381 validation.By(func(value interface{}) error { 382 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 383 }), 384 ), 385 validation.Field(&api.ResourceDefinitions, validation.By(func(value interface{}) error { 386 return validateAPIResourceDefinitions(value, *api, packagePolicyLevels) 387 })), 388 validation.Field(&api.APIResourceLinks, validation.By(validateAPILinks)), 389 validation.Field(&api.Links, validation.By(validateORDLinks)), 390 validation.Field(&api.ReleaseStatus, validation.Required, validation.In(ReleaseStatusBeta, ReleaseStatusActive, ReleaseStatusDeprecated)), 391 validation.Field(&api.SunsetDate, validation.When(*api.ReleaseStatus == ReleaseStatusDeprecated, validation.Required), validation.When(api.SunsetDate != nil, validation.By(isValidDate))), 392 validation.Field(&api.ChangeLogEntries, validation.By(validateORDChangeLogEntries)), 393 validation.Field(&api.TargetURLs, validation.By(validateEntryPoints), validation.When(api.TargetURLs == nil, validation.By(notPartOfConsumptionBundles(api.PartOfConsumptionBundles)))), 394 validation.Field(&api.Labels, validation.By(validateORDLabels)), 395 validation.Field(&api.ImplementationStandard, validation.In(APIImplementationStandardDocumentAPI, APIImplementationStandardServiceBroker, APIImplementationStandardCsnExposure, APIImplementationStandardApeAPI, APIImplementationStandardCdiAPI, APIImplementationStandardCustom)), 396 validation.Field(&api.CustomImplementationStandard, validation.When(api.ImplementationStandard != nil && *api.ImplementationStandard == APIImplementationStandardCustom, validation.Required, validation.Match(regexp.MustCompile(CustomImplementationStandardRegex))).Else(validation.Empty)), 397 validation.Field(&api.CustomImplementationStandardDescription, validation.When(api.ImplementationStandard != nil && *api.ImplementationStandard == APIImplementationStandardCustom, validation.Required).Else(validation.Empty)), 398 validation.Field(&api.PartOfConsumptionBundles, validation.By(func(value interface{}) error { 399 return validateAPIPartOfConsumptionBundles(value, api.TargetURLs, regexp.MustCompile(BundleOrdIDRegex)) 400 })), 401 validation.Field(&api.DefaultConsumptionBundle, validation.Match(regexp.MustCompile(BundleOrdIDRegex)), validation.By(func(value interface{}) error { 402 return validateDefaultConsumptionBundle(value, api.PartOfConsumptionBundles) 403 })), 404 validation.Field(&api.Extensible, validation.By(func(value interface{}) error { 405 return validateExtensibleField(value, api.OrdPackageID, packagePolicyLevels) 406 })), 407 validation.Field(&api.DocumentationLabels, validation.By(validateDocumentationLabels)), 408 ) 409 } 410 411 func validateEventInput(event *model.EventDefinitionInput, packagePolicyLevels map[string]string, eventsFromDB map[string]*model.EventDefinition, eventHashes map[string]uint64) error { 412 return validation.ValidateStruct(event, 413 validation.Field(&event.OrdID, validation.Required, validation.Match(regexp.MustCompile(EventOrdIDRegex))), 414 validation.Field(&event.LocalTenantID, validation.NilOrNotEmpty, validation.Length(MinLocalTenantIDLength, MaxLocalTenantIDLength)), 415 validation.Field(&event.Name, validation.Required), 416 validation.Field(&event.ShortDescription, shortDescriptionRules...), 417 validation.Field(&event.Description, validation.Required, validation.Length(MinDescriptionLength, MaxDescriptionLength)), 418 validation.Field(&event.PolicyLevel, validation.In(PolicyLevelSap, PolicyLevelSapPartner, PolicyLevelCustom, PolicyLevelNone), validation.When(event.CustomPolicyLevel != nil, validation.In(PolicyLevelCustom))), 419 validation.Field(&event.CustomPolicyLevel, validation.When(event.PolicyLevel != nil && *event.PolicyLevel != PolicyLevelCustom, validation.Empty), validation.Match(regexp.MustCompile(CustomPolicyLevelRegex))), 420 validation.Field(&event.VersionInput.Value, validation.Required, validation.Match(regexp.MustCompile(SemVerRegex)), validation.By(func(value interface{}) error { 421 return validateEventDefinitionVersionInput(value, *event, eventsFromDB, eventHashes) 422 })), 423 validation.Field(&event.OrdPackageID, validation.Required, validation.Match(regexp.MustCompile(PackageOrdIDRegex))), 424 validation.Field(&event.Visibility, validation.Required, validation.In(APIVisibilityPublic, APIVisibilityInternal, APIVisibilityPrivate)), 425 validation.Field(&event.PartOfProducts, validation.By(func(value interface{}) error { 426 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(ProductOrdIDRegex)) 427 })), 428 validation.Field(&event.Hierarchy, validation.By(func(value interface{}) error { 429 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 430 })), 431 validation.Field(&event.Tags, validation.By(func(value interface{}) error { 432 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 433 })), 434 validation.Field(&event.Countries, validation.By(func(value interface{}) error { 435 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CountryRegex)) 436 })), 437 validation.Field(&event.LineOfBusiness, 438 validation.By(func(value interface{}) error { 439 return validateWhenPolicyLevelIsSAP(event.OrdPackageID, packagePolicyLevels, func() error { 440 return validateJSONArrayOfStringsContainsInMap(value, LineOfBusinesses) 441 }) 442 }), 443 validation.By(func(value interface{}) error { 444 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 445 }), 446 ), 447 validation.Field(&event.Industry, 448 validation.By(func(value interface{}) error { 449 return validateWhenPolicyLevelIsSAP(event.OrdPackageID, packagePolicyLevels, func() error { 450 return validateJSONArrayOfStringsContainsInMap(value, Industries) 451 }) 452 }), 453 validation.By(func(value interface{}) error { 454 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 455 }), 456 ), 457 validation.Field(&event.ResourceDefinitions, validation.By(func(value interface{}) error { 458 return validateEventResourceDefinition(value, *event, packagePolicyLevels) 459 })), 460 validation.Field(&event.Links, validation.By(validateORDLinks)), 461 validation.Field(&event.ReleaseStatus, validation.Required, validation.In(ReleaseStatusBeta, ReleaseStatusActive, ReleaseStatusDeprecated)), 462 validation.Field(&event.SunsetDate, validation.When(*event.ReleaseStatus == ReleaseStatusDeprecated, validation.Required), validation.When(event.SunsetDate != nil, validation.By(isValidDate))), 463 validation.Field(&event.ChangeLogEntries, validation.By(validateORDChangeLogEntries)), 464 validation.Field(&event.Labels, validation.By(validateORDLabels)), 465 validation.Field(&event.PartOfConsumptionBundles, validation.By(func(value interface{}) error { 466 return validateEventPartOfConsumptionBundles(value, regexp.MustCompile(BundleOrdIDRegex)) 467 })), 468 validation.Field(&event.DefaultConsumptionBundle, validation.Match(regexp.MustCompile(BundleOrdIDRegex)), validation.By(func(value interface{}) error { 469 return validateDefaultConsumptionBundle(value, event.PartOfConsumptionBundles) 470 })), 471 validation.Field(&event.Extensible, validation.By(func(value interface{}) error { 472 return validateExtensibleField(value, event.OrdPackageID, packagePolicyLevels) 473 })), 474 validation.Field(&event.DocumentationLabels, validation.By(validateDocumentationLabels)), 475 ) 476 } 477 478 func validateProductInput(product *model.ProductInput) error { 479 productOrdIDNamespace := strings.Split(product.OrdID, ":")[0] 480 481 return validation.ValidateStruct(product, 482 validation.Field(&product.OrdID, validation.Required, validation.Match(regexp.MustCompile(ProductOrdIDRegex))), 483 validation.Field(&product.Title, validation.Length(1, 255), validation.Required), 484 validation.Field(&product.ShortDescription, shortDescriptionRules...), 485 validation.Field(&product.Vendor, validation.Required, 486 validation.Match(regexp.MustCompile(VendorOrdIDRegex)), 487 validation.When(regexp.MustCompile(SAPProductOrdIDNamespaceRegex).MatchString(productOrdIDNamespace), validation.In(SapVendor)).Else(validation.NotIn(SapVendor)), 488 ), 489 validation.Field(&product.Parent, validation.When(product.Parent != nil, validation.Match(regexp.MustCompile(ProductOrdIDRegex)))), 490 validation.Field(&product.CorrelationIDs, validation.By(func(value interface{}) error { 491 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(CorrelationIDsRegex)) 492 })), 493 validation.Field(&product.Labels, validation.By(validateORDLabels)), 494 validation.Field(&product.Tags, validation.By(func(value interface{}) error { 495 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 496 })), 497 validation.Field(&product.DocumentationLabels, validation.By(validateDocumentationLabels)), 498 ) 499 } 500 501 func validateVendorInput(vendor *model.VendorInput) error { 502 return validation.ValidateStruct(vendor, 503 validation.Field(&vendor.OrdID, validation.Required, validation.Match(regexp.MustCompile(VendorOrdIDRegex))), 504 validation.Field(&vendor.Title, validation.Length(1, 255), validation.Required), 505 validation.Field(&vendor.Labels, validation.By(validateORDLabels)), 506 validation.Field(&vendor.Partners, validation.By(func(value interface{}) error { 507 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(VendorPartnersRegex)) 508 })), 509 validation.Field(&vendor.Tags, validation.By(func(value interface{}) error { 510 return validateJSONArrayOfStringsMatchPattern(value, regexp.MustCompile(StringArrayElementRegex)) 511 })), 512 validation.Field(&vendor.DocumentationLabels, validation.By(validateDocumentationLabels)), 513 ) 514 } 515 516 func validateTombstoneInput(tombstone *model.TombstoneInput) error { 517 return validation.ValidateStruct(tombstone, 518 validation.Field(&tombstone.OrdID, validation.Required, validation.Match(regexp.MustCompile(TombstoneOrdIDRegex))), 519 validation.Field(&tombstone.RemovalDate, validation.Required, validation.By(isValidDate))) 520 } 521 522 func validateORDLabels(val interface{}) error { 523 return validateLabels(val, LabelsKeyRegex) 524 } 525 526 func validateDocumentationLabels(val interface{}) error { 527 return validateLabels(val, NoNewLineRegex) 528 } 529 530 func validateLabels(val interface{}, regex string) error { 531 if val == nil { 532 return nil 533 } 534 535 labels, ok := val.(json.RawMessage) 536 if !ok { 537 return errors.New("labels should be json") 538 } 539 540 if len(labels) == 0 { 541 return nil 542 } 543 544 if !gjson.ValidBytes(labels) { 545 return errors.New("labels should be valid json") 546 } 547 548 parsedLabels := gjson.ParseBytes(labels) 549 if !parsedLabels.IsObject() { 550 return errors.New("labels should be json object") 551 } 552 553 var err error 554 parsedLabels.ForEach(func(key, value gjson.Result) bool { 555 if err = validation.Validate(key.String(), validation.Match(regexp.MustCompile(regex))); err != nil { 556 return false 557 } 558 if !value.IsArray() { 559 err = errors.New("label value should be array") 560 return false 561 } 562 for _, el := range value.Array() { 563 if el.Type != gjson.String { 564 err = errors.New("label value should be array of strings") 565 return false 566 } 567 } 568 return true 569 }) 570 return err 571 } 572 573 func validateEntryPoints(val interface{}) error { 574 if val == nil { 575 return nil 576 } 577 578 entryPoints, ok := val.(json.RawMessage) 579 if !ok { 580 return errors.New("entryPoints should be json") 581 } 582 583 if len(entryPoints) == 0 { 584 return nil 585 } 586 587 if !gjson.ValidBytes(entryPoints) { 588 return errors.New("entryPoints should be valid json") 589 } 590 591 parsedArr := gjson.ParseBytes(entryPoints) 592 if !parsedArr.IsArray() { 593 return errors.New("should be json array") 594 } 595 596 if len(parsedArr.Array()) == 0 { 597 return errors.New("entryPoints should not be empty if present") 598 } 599 600 if areThereEntryPointDuplicates(parsedArr.Array()) { 601 return errors.New("entryPoints should not contain duplicates") 602 } 603 604 for _, el := range parsedArr.Array() { 605 if el.Type != gjson.String { 606 return errors.New("should be array of strings") 607 } 608 609 err := validation.Validate(el.String(), is.RequestURI) 610 if err != nil { 611 return errors.New("entryPoint should be a valid URI") 612 } 613 } 614 return nil 615 } 616 617 func validateORDChangeLogEntries(value interface{}) error { 618 return validateJSONArrayOfObjects(value, map[string][]validation.Rule{ 619 "version": { 620 validation.Required, 621 validation.Match(regexp.MustCompile(SemVerRegex)), 622 }, 623 "releaseStatus": { 624 validation.Required, 625 validation.In(ReleaseStatusBeta, ReleaseStatusActive, ReleaseStatusDeprecated), 626 }, 627 "date": { 628 validation.Required, 629 validation.By(isValidDate), 630 }, 631 "description": { 632 validation.NilOrNotEmpty, 633 validation.Length(MinDescriptionLength, MaxDescriptionLength), 634 }, 635 "url": { 636 is.RequestURI, 637 }, 638 }) 639 } 640 641 func validateORDLinks(value interface{}) error { 642 return validateJSONArrayOfObjects(value, map[string][]validation.Rule{ 643 "title": { 644 validation.Length(1, 255), 645 validation.Required, 646 }, 647 "url": { 648 validation.Required, 649 is.RequestURI, 650 }, 651 "description": { 652 validation.NilOrNotEmpty, 653 validation.Length(MinDescriptionLength, MaxDescriptionLength), 654 }, 655 }) 656 } 657 658 func validatePackageLinks(value interface{}) error { 659 return validateJSONArrayOfObjects(value, map[string][]validation.Rule{ 660 "type": { 661 validation.Required, 662 validation.In("terms-of-service", "license", "client-registration", "payment", "sandbox", "service-level-agreement", "support", "custom"), 663 }, 664 "url": { 665 validation.Required, 666 is.RequestURI, 667 }, 668 }, func(el gjson.Result) error { 669 if el.Get("customType").Exists() { 670 if el.Get("type").String() != custom { 671 return errors.New("if customType is provided, type should be set to 'custom'") 672 } else { 673 return validation.Validate(el.Get("customType").String(), validation.Match(regexp.MustCompile(CustomImplementationStandardRegex))) 674 } 675 } 676 return nil 677 }) 678 } 679 680 func validateAPILinks(value interface{}) error { 681 return validateJSONArrayOfObjects(value, map[string][]validation.Rule{ 682 "type": { 683 validation.Required, 684 validation.In("api-documentation", "authentication", "client-registration", "console", "payment", "service-level-agreement", "support", "custom"), 685 }, 686 "url": { 687 validation.Required, 688 is.RequestURI, 689 }, 690 }, func(el gjson.Result) error { 691 if el.Get("customType").Exists() { 692 if el.Get("type").String() != custom { 693 return errors.New("if customType is provided, type should be set to 'custom'") 694 } else { 695 return validation.Validate(el.Get("customType").String(), validation.Match(regexp.MustCompile(CustomImplementationStandardRegex))) 696 } 697 } 698 return nil 699 }) 700 } 701 702 func validateAPIResourceDefinitions(value interface{}, api model.APIDefinitionInput, packagePolicyLevels map[string]string) error { 703 if value == nil { 704 return nil 705 } 706 707 pkgOrdID := str.PtrStrToStr(api.OrdPackageID) 708 policyLevel := packagePolicyLevels[pkgOrdID] 709 apiVisibility := str.PtrStrToStr(api.Visibility) 710 apiProtocol := str.PtrStrToStr(api.APIProtocol) 711 resourceDefinitions := api.ResourceDefinitions 712 713 isResourceDefinitionMandatory := !(policyLevel == PolicyLevelSap && apiVisibility == APIVisibilityPrivate) 714 if len(resourceDefinitions) == 0 && isResourceDefinitionMandatory { 715 return errors.New("when api resource visibility is public or internal, resource definitions must be provided") 716 } 717 718 if len(resourceDefinitions) == 0 && !isResourceDefinitionMandatory { 719 return nil 720 } 721 722 resourceDefinitionTypes := make(map[model.APISpecType]bool) 723 724 for _, rd := range resourceDefinitions { 725 resourceDefinitionType := rd.Type 726 resourceDefinitionTypes[resourceDefinitionType] = true 727 } 728 729 isPolicyCoreOrPartner := policyLevel == PolicyLevelSap || policyLevel == PolicyLevelSapPartner 730 wsdlTypeExists := resourceDefinitionTypes[model.APISpecTypeWsdlV1] || resourceDefinitionTypes[model.APISpecTypeWsdlV2] 731 if isPolicyCoreOrPartner && (apiProtocol == APIProtocolSoapInbound || apiProtocol == APIProtocolSoapOutbound) && !wsdlTypeExists { 732 return errors.New("for APIResources of policyLevel='sap' or 'sap-partner' and with apiProtocol='soap-inbound' or 'soap-outbound' it is mandatory to provide either WSDL V2 or WSDL V1 definitions") 733 } 734 735 edmxTypeExists := resourceDefinitionTypes[model.APISpecTypeEDMX] 736 openAPITypeExists := resourceDefinitionTypes[model.APISpecTypeOpenAPIV2] || resourceDefinitionTypes[model.APISpecTypeOpenAPIV3] 737 if isPolicyCoreOrPartner && (apiProtocol == APIProtocolODataV2 || apiProtocol == APIProtocolODataV4) && !(edmxTypeExists && openAPITypeExists) { 738 return errors.New("for APIResources of policyLevel='sap' or 'sap-partner' and with apiProtocol='odata-v2' or 'odata-v4' it is mandatory to not only provide edmx definitions, but also OpenAPI definitions") 739 } 740 741 if isPolicyCoreOrPartner && apiProtocol == APIProtocolRest && !openAPITypeExists { 742 return errors.New("for APIResources of policyLevel='sap' or 'sap-partner' and with apiProtocol='rest' it is mandatory to provide either OpenAPI 3 or OpenAPI 2 definitions") 743 } 744 745 rfcMetadataTypeExists := resourceDefinitionTypes[model.APISpecTypeRfcMetadata] 746 if isPolicyCoreOrPartner && apiProtocol == APIProtocolSapRfc && !rfcMetadataTypeExists { 747 return errors.New("for APIResources of policyLevel='sap' or 'sap-partner' and with apiProtocol='sap-rfc' it is mandatory to provide SAP RFC definitions") 748 } 749 750 if apiProtocol == APIProtocolWebsocket && (api.ImplementationStandard == nil || !resourceDefinitionTypes[model.APISpecTypeCustom]) { 751 return errors.New("for APIResources with apiProtocol='websocket' it is mandatory to provide implementationStandard definition and type to be set to custom") 752 } 753 754 if apiProtocol == APIProtocolSAPSQLAPIV1 && !(resourceDefinitionTypes[model.APISpecTypeCustom] || resourceDefinitionTypes[model.APISpecTypeSQLAPIDefinitionV1]) { 755 return errors.New("for APIResources with apiProtocol='sap-sql-api-v1' it is mandatory type to be set either to sap-sql-api-definition-v1 or custom") 756 } 757 758 return nil 759 } 760 761 func validateEventResourceDefinition(value interface{}, event model.EventDefinitionInput, packagePolicyLevels map[string]string) error { 762 if value == nil { 763 return nil 764 } 765 766 pkgOrdID := str.PtrStrToStr(event.OrdPackageID) 767 policyLevel := packagePolicyLevels[pkgOrdID] 768 apiVisibility := str.PtrStrToStr(event.Visibility) 769 770 if policyLevel == PolicyLevelSap && apiVisibility == APIVisibilityPrivate { 771 return nil 772 } 773 774 eventResourceDef, ok := value.([]*model.EventResourceDefinition) 775 if !ok { 776 return errors.New("error while casting to EventResourceDefinition") 777 } 778 779 if len(eventResourceDef) == 0 { 780 return errors.New("when event resource visibility is public or internal, resource definitions must be provided") 781 } 782 783 return nil 784 } 785 786 func validatePackageVersionInput(value interface{}, pkg model.PackageInput, pkgsFromDB map[string]*model.Package, resourceHashes map[string]uint64) error { 787 if value == nil { 788 return nil 789 } 790 791 if len(pkgsFromDB) == 0 { 792 return nil 793 } 794 795 pkgFromDB, ok := pkgsFromDB[pkg.OrdID] 796 if !ok || isResourceHashMissing(pkgFromDB.ResourceHash) { 797 return nil 798 } 799 800 hashDB := str.PtrStrToStr(pkgFromDB.ResourceHash) 801 hashDoc := strconv.FormatUint(resourceHashes[pkg.OrdID], 10) 802 803 return checkHashEquality(pkgFromDB.Version, pkg.Version, hashDB, hashDoc) 804 } 805 806 func validateBundleVersionInput(value interface{}, bndl model.BundleCreateInput, bndlsFromDB map[string]*model.Bundle, resourceHashes map[string]uint64) error { 807 if value == nil { 808 return nil 809 } 810 811 if len(bndlsFromDB) == 0 { 812 return nil 813 } 814 815 bndlOrdID := str.PtrStrToStr(bndl.OrdID) 816 bndlFromDB, ok := bndlsFromDB[bndlOrdID] 817 if !ok || isResourceHashMissing(bndlFromDB.ResourceHash) { 818 return nil 819 } 820 821 hashDB := str.PtrStrToStr(bndlFromDB.ResourceHash) 822 hashDoc := strconv.FormatUint(resourceHashes[str.PtrStrToStr(bndl.OrdID)], 10) 823 824 if bndlFromDB.Version != nil && bndl.Version != nil { 825 return checkHashEquality(*bndlFromDB.Version, *bndl.Version, hashDB, hashDoc) 826 } 827 if bndlFromDB.Version != nil && bndl.Version == nil { 828 return errors.New("bundle version is present in the DB, but is missing from the document") 829 } 830 return nil 831 } 832 833 func validateEventDefinitionVersionInput(value interface{}, event model.EventDefinitionInput, eventsFromDB map[string]*model.EventDefinition, eventHashes map[string]uint64) error { 834 if value == nil { 835 return nil 836 } 837 838 if len(eventsFromDB) == 0 { 839 return nil 840 } 841 842 eventFromDB, ok := eventsFromDB[str.PtrStrToStr(event.OrdID)] 843 if !ok || isResourceHashMissing(eventFromDB.ResourceHash) { 844 return nil 845 } 846 847 hashDB := str.PtrStrToStr(eventFromDB.ResourceHash) 848 hashDoc := strconv.FormatUint(eventHashes[str.PtrStrToStr(event.OrdID)], 10) 849 850 return checkHashEquality(eventFromDB.Version.Value, event.VersionInput.Value, hashDB, hashDoc) 851 } 852 853 func validateAPIDefinitionVersionInput(value interface{}, api model.APIDefinitionInput, apisFromDB map[string]*model.APIDefinition, apiHashes map[string]uint64) error { 854 if value == nil { 855 return nil 856 } 857 858 if len(apisFromDB) == 0 { 859 return nil 860 } 861 862 apiFromDB, ok := apisFromDB[str.PtrStrToStr(api.OrdID)] 863 if !ok || isResourceHashMissing(apiFromDB.ResourceHash) { 864 return nil 865 } 866 867 hashDB := str.PtrStrToStr(apiFromDB.ResourceHash) 868 hashDoc := strconv.FormatUint(apiHashes[str.PtrStrToStr(api.OrdID)], 10) 869 870 return checkHashEquality(apiFromDB.Version.Value, api.VersionInput.Value, hashDB, hashDoc) 871 } 872 873 func normalizeAPIDefinition(api *model.APIDefinitionInput) (model.APIDefinitionInput, error) { 874 bytes, err := json.Marshal(api) 875 if err != nil { 876 return model.APIDefinitionInput{}, errors.Wrapf(err, "error while marshalling api definition with ID %s", str.PtrStrToStr(api.OrdID)) 877 } 878 879 var normalizedAPIDefinition model.APIDefinitionInput 880 if err := json.Unmarshal(bytes, &normalizedAPIDefinition); err != nil { 881 return model.APIDefinitionInput{}, errors.Wrapf(err, "error while unmarshalling api definition with ID %s", str.PtrStrToStr(api.OrdID)) 882 } 883 884 return normalizedAPIDefinition, nil 885 } 886 887 func normalizeEventDefinition(event *model.EventDefinitionInput) (model.EventDefinitionInput, error) { 888 bytes, err := json.Marshal(event) 889 if err != nil { 890 return model.EventDefinitionInput{}, errors.Wrapf(err, "error while marshalling event definition with ID %s", str.PtrStrToStr(event.OrdID)) 891 } 892 893 var normalizedEventDefinition model.EventDefinitionInput 894 if err := json.Unmarshal(bytes, &normalizedEventDefinition); err != nil { 895 return model.EventDefinitionInput{}, errors.Wrapf(err, "error while unmarshalling event definition with ID %s", str.PtrStrToStr(event.OrdID)) 896 } 897 898 return normalizedEventDefinition, nil 899 } 900 901 func normalizePackage(pkg *model.PackageInput) (model.PackageInput, error) { 902 bytes, err := json.Marshal(pkg) 903 if err != nil { 904 return model.PackageInput{}, errors.Wrapf(err, "error while marshalling package definition with ID %s", pkg.OrdID) 905 } 906 907 var normalizedPkgDefinition model.PackageInput 908 if err := json.Unmarshal(bytes, &normalizedPkgDefinition); err != nil { 909 return model.PackageInput{}, errors.Wrapf(err, "error while unmarshalling package definition with ID %s", pkg.OrdID) 910 } 911 912 return normalizedPkgDefinition, nil 913 } 914 915 func normalizeBundle(bndl *model.BundleCreateInput) (model.BundleCreateInput, error) { 916 bytes, err := json.Marshal(bndl) 917 if err != nil { 918 return model.BundleCreateInput{}, errors.Wrapf(err, "error while marshalling bundle definition with ID %v", bndl.OrdID) 919 } 920 921 var normalizedBndlDefinition model.BundleCreateInput 922 if err := json.Unmarshal(bytes, &normalizedBndlDefinition); err != nil { 923 return model.BundleCreateInput{}, errors.Wrapf(err, "error while unmarshalling bundle definition with ID %v", bndl.OrdID) 924 } 925 926 return normalizedBndlDefinition, nil 927 } 928 929 func isResourceHashMissing(hash *string) bool { 930 hashStr := str.PtrStrToStr(hash) 931 return hashStr == "" 932 } 933 934 func noNewLines(s string) bool { 935 return !strings.Contains(s, "\\n") 936 } 937 938 func validateWhenPolicyLevelIsSAP(packageOrdID *string, packagePolicyLevels map[string]string, validationFunc func() error) error { 939 pkgOrdID := str.PtrStrToStr(packageOrdID) 940 policyLevel := packagePolicyLevels[pkgOrdID] 941 942 if policyLevel != PolicyLevelSap { 943 return nil 944 } 945 946 return validationFunc() 947 } 948 949 func validateJSONArrayOfStringsContainsInMap(arr interface{}, validValues map[string]bool) error { 950 if arr == nil { 951 return nil 952 } 953 954 jsonArr, ok := arr.(json.RawMessage) 955 if !ok { 956 return errors.New("should be json") 957 } 958 959 if len(jsonArr) == 0 { 960 return nil 961 } 962 963 if !gjson.ValidBytes(jsonArr) { 964 return errors.New("should be valid json") 965 } 966 967 parsedArr := gjson.ParseBytes(jsonArr) 968 if !parsedArr.IsArray() { 969 return errors.New("should be json array") 970 } 971 972 for _, el := range parsedArr.Array() { 973 if el.Type != gjson.String { 974 return errors.New("should be array of strings") 975 } 976 977 exists, ok := validValues[el.String()] 978 979 if !exists || !ok { 980 return errors.New("array element is not in the list of valid values") 981 } 982 } 983 984 return nil 985 } 986 987 func validateJSONArrayOfStringsMatchPattern(arr interface{}, regexPattern *regexp.Regexp) error { 988 if arr == nil { 989 return nil 990 } 991 992 jsonArr, ok := arr.(json.RawMessage) 993 if !ok { 994 return errors.New("should be json") 995 } 996 997 if len(jsonArr) == 0 { 998 return nil 999 } 1000 1001 if !gjson.ValidBytes(jsonArr) { 1002 return errors.New("should be valid json") 1003 } 1004 parsedArr := gjson.ParseBytes(jsonArr) 1005 1006 if !parsedArr.IsArray() { 1007 return errors.New("should be json array") 1008 } 1009 1010 if len(parsedArr.Array()) == 0 { 1011 return nil 1012 } 1013 1014 for _, el := range parsedArr.Array() { 1015 if el.Type != gjson.String { 1016 return errors.New("should be array of strings") 1017 } 1018 if !regexPattern.MatchString(el.String()) { 1019 return errors.Errorf("elements should match %q", regexPattern.String()) 1020 } 1021 } 1022 return nil 1023 } 1024 1025 func validateJSONArrayOfObjects(arr interface{}, elementFieldRules map[string][]validation.Rule, crossFieldRules ...func(gjson.Result) error) error { 1026 if arr == nil { 1027 return nil 1028 } 1029 1030 jsonArr, ok := arr.(json.RawMessage) 1031 if !ok { 1032 return errors.New("should be json") 1033 } 1034 1035 if len(jsonArr) == 0 { 1036 return nil 1037 } 1038 1039 if !gjson.ValidBytes(jsonArr) { 1040 return errors.New("should be valid json") 1041 } 1042 1043 parsedArr := gjson.ParseBytes(jsonArr) 1044 if !parsedArr.IsArray() { 1045 return errors.New("should be json array") 1046 } 1047 1048 if len(parsedArr.Array()) == 0 { 1049 return nil 1050 } 1051 1052 for _, el := range parsedArr.Array() { 1053 for field, rules := range elementFieldRules { 1054 if err := validation.Validate(el.Get(field).Value(), rules...); err != nil { 1055 return errors.Wrapf(err, "error validating field %s", field) 1056 } 1057 for _, f := range crossFieldRules { 1058 if err := f(el); err != nil { 1059 return err 1060 } 1061 } 1062 } 1063 } 1064 1065 return nil 1066 } 1067 1068 func validateJSONObjects(obj interface{}, elementFieldRules map[string][]validation.Rule, crossFieldRules ...func(gjson.Result) error) error { 1069 if obj == nil { 1070 return nil 1071 } 1072 1073 jsonObj, ok := obj.(json.RawMessage) 1074 if !ok { 1075 return errors.New("should be json") 1076 } 1077 1078 if len(jsonObj) == 0 { 1079 return nil 1080 } 1081 1082 if !gjson.ValidBytes(jsonObj) { 1083 return errors.New("should be valid json") 1084 } 1085 1086 parsedObj := gjson.ParseBytes(jsonObj) 1087 if !parsedObj.IsObject() { 1088 return errors.New("should be json object") 1089 } 1090 1091 for field, rules := range elementFieldRules { 1092 if err := validation.Validate(parsedObj.Get(field).Value(), rules...); err != nil { 1093 return errors.Wrapf(err, "error validating field %s", field) 1094 } 1095 for _, f := range crossFieldRules { 1096 if err := f(parsedObj); err != nil { 1097 return err 1098 } 1099 } 1100 } 1101 1102 return nil 1103 } 1104 1105 func validateCustomType(credentialExchangeStrategyTenantMappings map[string]CredentialExchangeStrategyTenantMapping) func(el gjson.Result) error { 1106 return func(el gjson.Result) error { 1107 if el.Get("customType").Exists() && el.Get("type").String() != custom { 1108 return errors.New("if customType is provided, type should be set to 'custom'") 1109 } 1110 1111 customType := el.Get("customType").String() 1112 if _, ok := credentialExchangeStrategyTenantMappings[customType]; strings.Contains(customType, TenantMappingCustomTypeIdentifier) && !ok { 1113 return errors.New("credential exchange strategy's tenant mapping customType is not valid") 1114 } 1115 1116 return validation.Validate(customType, validation.Match(regexp.MustCompile(CustomTypeCredentialExchangeStrategyRegex))) 1117 } 1118 } 1119 1120 func validateCustomDescription(el gjson.Result) error { 1121 if el.Get("customDescription").Exists() && el.Get("type").String() != custom { 1122 return errors.New("if customDescription is provided, type should be set to 'custom'") 1123 } 1124 if el.Get("customDescription").Exists() && el.Get("type").String() == custom && (len(el.Get("customDescription").String()) < MinDescriptionLength || len(el.Get("customDescription").String()) > MaxDescriptionLength) { 1125 return errors.New(fmt.Sprintf("if customDescription is provided and type is 'custom', then the accepted length of customDescription is between %d - %d characters", MinDescriptionLength, MaxDescriptionLength)) 1126 } 1127 return nil 1128 } 1129 1130 func validateEventPartOfConsumptionBundles(value interface{}, regexPattern *regexp.Regexp) error { 1131 bundleReferences, ok := value.([]*model.ConsumptionBundleReference) 1132 if !ok { 1133 return errors.New("error while casting to ConsumptionBundleReference") 1134 } 1135 1136 if bundleReferences != nil && len(bundleReferences) == 0 { 1137 return errors.New("bundleReference should not be empty if present") 1138 } 1139 1140 bundleIDsPerEvent := make(map[string]bool) 1141 for _, br := range bundleReferences { 1142 if br.BundleOrdID == "" { 1143 return errors.New("bundleReference ordId is mandatory field") 1144 } 1145 1146 if !regexPattern.MatchString(br.BundleOrdID) { 1147 return errors.Errorf("ordId should match %q", regexPattern.String()) 1148 } 1149 1150 if isPresent := bundleIDsPerEvent[br.BundleOrdID]; !isPresent { 1151 bundleIDsPerEvent[br.BundleOrdID] = true 1152 } else { 1153 return errors.Errorf("event can not reference the same bundle with ordId %q more than once", br.BundleOrdID) 1154 } 1155 1156 if br.DefaultTargetURL != "" { 1157 return errors.New("events are not supposed to have defaultEntryPoint") 1158 } 1159 } 1160 return nil 1161 } 1162 1163 func validateAPIPartOfConsumptionBundles(value interface{}, targetURLs json.RawMessage, regexPattern *regexp.Regexp) error { 1164 bundleReferences, ok := value.([]*model.ConsumptionBundleReference) 1165 if !ok { 1166 return errors.New("error while casting to ConsumptionBundleReference") 1167 } 1168 1169 if bundleReferences != nil && len(bundleReferences) == 0 { 1170 return errors.New("bundleReference should not be empty if present") 1171 } 1172 1173 bundleIDsPerAPI := make(map[string]bool) 1174 for _, br := range bundleReferences { 1175 if br.BundleOrdID == "" { 1176 return errors.New("bundleReference ordId is mandatory field") 1177 } 1178 1179 if !regexPattern.MatchString(br.BundleOrdID) { 1180 return errors.Errorf("ordId should match %q", regexPattern.String()) 1181 } 1182 1183 if isPresent := bundleIDsPerAPI[br.BundleOrdID]; !isPresent { 1184 bundleIDsPerAPI[br.BundleOrdID] = true 1185 } else { 1186 return errors.Errorf("api can not reference the same bundle with ordId %q more than once", br.BundleOrdID) 1187 } 1188 1189 err := validation.Validate(br.DefaultTargetURL, is.RequestURI) 1190 if err != nil { 1191 return errors.New("defaultEntryPoint should be a valid URI") 1192 } 1193 1194 lenTargetURLs := len(gjson.ParseBytes(targetURLs).Array()) 1195 if br.DefaultTargetURL != "" && lenTargetURLs <= 1 { 1196 return errors.New("defaultEntryPoint must only be provided if an API has more than one entry point") 1197 } 1198 1199 if br.DefaultTargetURL != "" && lenTargetURLs > 1 { 1200 if isDefaultTargetURLMissingFromTargetURLs(br.DefaultTargetURL, targetURLs) { 1201 return errors.New("defaultEntryPoint must be in the list of entryPoints for the given API") 1202 } 1203 } 1204 } 1205 1206 return nil 1207 } 1208 1209 func validateDefaultConsumptionBundle(value interface{}, partOfConsumptionBundles []*model.ConsumptionBundleReference) error { 1210 defaultConsumptionBundle, ok := value.(*string) 1211 if !ok { 1212 return errors.New(fmt.Sprintf("expected string value for defaultConsumptionBundle, found %T", value)) 1213 } 1214 1215 if defaultConsumptionBundle == nil { 1216 return nil 1217 } 1218 1219 var isFound bool 1220 for _, bundleRef := range partOfConsumptionBundles { 1221 if *defaultConsumptionBundle == bundleRef.BundleOrdID { 1222 isFound = true 1223 break 1224 } 1225 } 1226 1227 if !isFound { 1228 return errors.New("defaultConsumptionBundle must be an existing option in the corresponding partOfConsumptionBundles array") 1229 } 1230 return nil 1231 } 1232 1233 func isDefaultTargetURLMissingFromTargetURLs(defaultTargetURL string, targetURLs json.RawMessage) bool { 1234 for _, targetURL := range gjson.ParseBytes(targetURLs).Array() { 1235 if targetURL.String() == defaultTargetURL { 1236 return false 1237 } 1238 } 1239 return true 1240 } 1241 1242 func areThereEntryPointDuplicates(entryPoints []gjson.Result) bool { 1243 if len(entryPoints) <= 1 { 1244 return false 1245 } 1246 1247 seen := make(map[string]bool) 1248 for _, val := range entryPoints { 1249 if seen[val.String()] { 1250 return true 1251 } 1252 seen[val.String()] = true 1253 } 1254 return false 1255 } 1256 1257 func isValidDate(d interface{}) error { 1258 var err error 1259 date, err := castDate(d) 1260 if err != nil { 1261 return err 1262 } 1263 1264 if _, err = time.Parse(time.RFC3339, date); err == nil { // RFC3339 -> "2006-01-02T15:04:05Z" or "2006-01-02T15:04:05+07:00" 1265 return nil 1266 } else if _, err = time.Parse("2006-01-02", date); err == nil { // RFC3339 date without time extension 1267 return nil 1268 } else if _, err = time.Parse("2006-01-02T15:04:05", date); err == nil { // ISO8601 without Z/00+00 1269 return nil 1270 } else if _, err = time.Parse("2006-01-02T15:04:05Z0700", date); err == nil { // ISO8601 with skipped ':' in offset (e.g.: 2006-01-02T15:04:05+0700) 1271 return nil 1272 } 1273 return errors.New("invalid date") 1274 } 1275 1276 func castDate(d interface{}) (string, error) { 1277 datePtr, ok := d.(*string) 1278 if ok { 1279 return *datePtr, nil 1280 } 1281 1282 date, ok := d.(string) 1283 if ok { 1284 return date, nil 1285 } 1286 1287 return "", errors.New(fmt.Sprintf("expected string or *string value for date, found %T", d)) 1288 } 1289 1290 func notPartOfConsumptionBundles(partOfConsumptionBundles []*model.ConsumptionBundleReference) validation.RuleFunc { 1291 return func(value interface{}) error { 1292 if len(partOfConsumptionBundles) > 0 { 1293 return errors.New("api without entry points can not be part of consumption bundle") 1294 } 1295 return nil 1296 } 1297 } 1298 1299 func validateExtensibleField(value interface{}, ordPackageID *string, packagePolicyLevels map[string]string) error { 1300 pkgOrdID := str.PtrStrToStr(ordPackageID) 1301 policyLevel := packagePolicyLevels[pkgOrdID] 1302 1303 if (policyLevel == PolicyLevelSap || policyLevel == PolicyLevelSapPartner) && (value == nil || value.(json.RawMessage) == nil) { 1304 return errors.Errorf("`extensible` field must be provided when `policyLevel` is either `%s` or `%s`", PolicyLevelSap, PolicyLevelSapPartner) 1305 } 1306 1307 return validateJSONObjects(value, map[string][]validation.Rule{ 1308 "supported": { 1309 validation.Required, 1310 validation.In("no", "manual", "automatic"), 1311 }, 1312 "description": {}, 1313 }, validateExtensibleInnerFields) 1314 } 1315 1316 func validateExtensibleInnerFields(el gjson.Result) error { 1317 supportedProperty := el.Get("supported") 1318 supportedValue, ok := supportedProperty.Value().(string) 1319 if !ok { 1320 return errors.New("`supported` value not provided") 1321 } 1322 1323 descriptionProperty := el.Get("description") 1324 descriptionValue, ok := descriptionProperty.Value().(string) 1325 validLength := len(descriptionValue) >= MinDescriptionLength && len(descriptionValue) <= MaxDescriptionLength 1326 1327 if supportedProperty.Exists() && (supportedValue == "manual" || supportedValue == "automatic") && (!validLength || !ok) { 1328 return errors.New(fmt.Sprintf("if supported field is either 'manual' or 'automatic', description should be provided with length of %d - %d characters", MinDescriptionLength, MaxDescriptionLength)) 1329 } 1330 return nil 1331 } 1332 1333 // HashObject hashes the given object 1334 func HashObject(obj interface{}) (uint64, error) { 1335 hash, err := hashstructure.Hash(obj, hashstructure.FormatV2, &hashstructure.HashOptions{SlicesAsSets: true}) 1336 if err != nil { 1337 return 0, errors.New("failed to hash the given object") 1338 } 1339 1340 return hash, nil 1341 } 1342 1343 func checkHashEquality(rdFromDBVersion, rdFromDocVersion, hashFromDB, hashFromDoc string) error { 1344 rdFromDBVersion = fmt.Sprintf("v%s", rdFromDBVersion) 1345 rdFromDocVersion = fmt.Sprintf("v%s", rdFromDocVersion) 1346 1347 areVersionsEqual := semver.Compare(rdFromDocVersion, rdFromDBVersion) 1348 if areHashesEqual := cmp.Equal(hashFromDB, hashFromDoc); !areHashesEqual && areVersionsEqual <= 0 { 1349 return errors.New("there is a change in the resource; version value should be incremented") 1350 } 1351 1352 return nil 1353 }