github.com/Axway/agent-sdk@v1.1.101/pkg/agent/handler/credential.go (about) 1 package handler 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "reflect" 8 "strings" 9 "time" 10 11 v1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1" 12 management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" 13 defs "github.com/Axway/agent-sdk/pkg/apic/definitions" 14 prov "github.com/Axway/agent-sdk/pkg/apic/provisioning" 15 "github.com/Axway/agent-sdk/pkg/apic/provisioning/idp" 16 "github.com/Axway/agent-sdk/pkg/authz/oauth" 17 "github.com/Axway/agent-sdk/pkg/util" 18 "github.com/Axway/agent-sdk/pkg/util/log" 19 "github.com/Axway/agent-sdk/pkg/watchmanager/proto" 20 ) 21 22 const ( 23 update = "update" 24 xAxwayEncrypted = "x-axway-encrypted" 25 crFinalizer = "agent.credential.provisioned" 26 ) 27 28 type credProv interface { 29 CredentialProvision(credentialRequest prov.CredentialRequest) (status prov.RequestStatus, credentials prov.Credential) 30 CredentialDeprovision(credentialRequest prov.CredentialRequest) (status prov.RequestStatus) 31 CredentialUpdate(credentialRequest prov.CredentialRequest) (status prov.RequestStatus, credentials prov.Credential) 32 } 33 34 type credentials struct { 35 marketplaceHandler 36 prov credProv 37 client client 38 encryptSchema encryptSchemaFunc 39 idpProviderRegistry oauth.IdPRegistry 40 } 41 42 // encryptSchemaFunc func signature for encryptSchema 43 type encryptSchemaFunc func(schema, credData map[string]interface{}, key, alg, hash string) (map[string]interface{}, error) 44 45 // NewCredentialHandler creates a Handler for Credentials 46 func NewCredentialHandler(prov credProv, client client, providerRegistry oauth.IdPRegistry) Handler { 47 return &credentials{ 48 prov: prov, 49 client: client, 50 encryptSchema: encryptSchema, 51 idpProviderRegistry: providerRegistry, 52 } 53 } 54 55 // Handle processes grpc events triggered for Credentials 56 func (h *credentials) Handle(ctx context.Context, meta *proto.EventMeta, resource *v1.ResourceInstance) error { 57 action := GetActionFromContext(ctx) 58 if resource.Kind != management.CredentialGVK().Kind || h.prov == nil || h.shouldIgnoreSubResourceUpdate(action, meta) { 59 return nil 60 } 61 62 logger := getLoggerFromContext(ctx).WithComponent("credentialHandler") 63 ctx = setLoggerInContext(ctx, logger) 64 65 cr := &management.Credential{} 66 err := cr.FromInstance(resource) 67 if err != nil { 68 logger.WithError(err).Error("could not handle credential request") 69 return nil 70 } 71 72 if ok := isStatusFound(cr.Status); !ok { 73 logger.Debugf("could not handle credential request as it did not have a status subresource") 74 return nil 75 } 76 77 if ok := h.shouldProcessDeleting(cr); ok { 78 logger.Trace("processing resource in deleting state") 79 h.onDeleting(ctx, cr) 80 return nil 81 } 82 83 var credential *management.Credential 84 if ok := h.shouldProcessPending(cr); ok { 85 log.Trace("processing resource in pending status") 86 credential = h.onPending(ctx, cr) 87 } else if actions := h.shouldProcessUpdating(cr); len(actions) != 0 { 88 log.Trace("processing resource in updating status") 89 credential = h.onUpdates(ctx, cr, actions) 90 } 91 92 if credential != nil { 93 err = h.client.CreateSubResource(cr.ResourceMeta, cr.SubResources) 94 if err != nil { 95 logger.WithError(err).Error("error creating subresources") 96 } 97 98 // update the status resource regardless of errors updating the other subresources 99 statusErr := h.client.CreateSubResource(credential.ResourceMeta, map[string]interface{}{"status": credential.Status}) 100 if statusErr != nil { 101 logger.WithError(statusErr).Error("error creating status subresources") 102 return statusErr 103 } 104 } 105 106 return err 107 } 108 109 // shouldProcessDeleting 110 // Finalizers = has agent finalizer and 111 // (Spec.State.Name = Inactive, StateReason = Credential Expired, Status.Level = Pending) or 112 // (Metadata.State = Deleting) 113 114 func (h *credentials) shouldProcessDeleting(cr *management.Credential) bool { 115 if !hasAgentCredentialFinalizer(cr.Finalizers) { 116 return false 117 } 118 119 if cr.Spec.State.Name == v1.Inactive && cr.Spec.State.Reason == prov.CredExpDetail && cr.Status.Level == prov.Pending.String() { 120 // expired credential 121 return true 122 } 123 124 if cr.Metadata.State == v1.ResourceDeleting { 125 // don't process delete when error from agent 126 return !hasAgentCredentialError(cr.Status) 127 } 128 129 return false 130 } 131 132 // shouldProvision 133 // Status.Level = Pending and 134 // Metadata.State = !Deleting and 135 // Spec.State.Name = Active and 136 // Spec.State.Rotate = false and 137 // Finalizers = no agent finalizer 138 func (h *credentials) shouldProcessPending(cr *management.Credential) bool { 139 if h.marketplaceHandler.shouldProcessPending(cr.Status, cr.Metadata.State) { 140 return cr.Spec.State.Name == v1.Active && !cr.Spec.State.Rotate && !hasAgentCredentialFinalizer(cr.Finalizers) 141 } 142 return false 143 } 144 145 // shouldProcessUpdating 146 func (h *credentials) shouldProcessUpdating(cr *management.Credential) []prov.CredentialAction { 147 actions := []prov.CredentialAction{} 148 inter := reflect.TypeOf((*credProv)(nil)).Elem() 149 if !reflect.TypeOf(h.prov).Implements(inter) { 150 log.Debugf("credential updates not supported by agent") 151 return actions 152 } 153 154 if !hasAgentCredentialFinalizer(cr.Finalizers) || cr.Status.Level != prov.Pending.String() { 155 return actions 156 } 157 158 // suspend 159 if cr.Spec.State.Name == v1.Inactive && (cr.State.Name == v1.Active || cr.State.Name == "") { 160 actions = append(actions, prov.Suspend) 161 } 162 163 // rotate 164 if cr.Spec.State.Rotate { 165 actions = append(actions, prov.Rotate) 166 } 167 168 // enable 169 if cr.Spec.State.Name == v1.Active && cr.State.Name == v1.Inactive { 170 actions = append(actions, prov.Enable) 171 } 172 return actions 173 } 174 175 func (h *credentials) onDeleting(ctx context.Context, cred *management.Credential) { 176 logger := getLoggerFromContext(ctx) 177 provData := h.deprovisionPreProcess(ctx, cred) 178 crd, err := h.getCRD(ctx, cred) 179 if err != nil { 180 logger.WithError(err).Error("error getting credential request definition") 181 h.onError(ctx, cred, err) 182 return 183 } 184 app, err := h.getManagedApp(ctx, cred) 185 if err != nil { 186 logger.WithError(err).Error("error getting managed app") 187 h.onError(ctx, cred, err) 188 return 189 } 190 191 provCreds, err := h.newProvCreds(cred, app, provData, 0, crd) 192 193 if err != nil { 194 logger.WithError(err).Error("error preparing credential request") 195 h.onError(ctx, cred, err) 196 return 197 } 198 199 status := h.prov.CredentialDeprovision(provCreds) 200 201 h.deprovisionPostProcess(status, provCreds, logger, ctx, cred) 202 } 203 204 func (*credentials) deprovisionPreProcess(_ context.Context, cred *management.Credential) map[string]interface{} { 205 var provData map[string]interface{} 206 if cred.Data != nil { 207 if m, ok := cred.Data.(map[string]interface{}); ok { 208 provData = m 209 } 210 } 211 return provData 212 } 213 214 func (h *credentials) deprovisionPostProcess(status prov.RequestStatus, provCreds *provCreds, logger log.FieldLogger, ctx context.Context, cred *management.Credential) { 215 if status.GetStatus() == prov.Success { 216 if provCreds.IsIDPCredential() { 217 err := provCreds.idpProvisioner.UnregisterClient() 218 if err != nil { 219 logger. 220 WithError(err). 221 WithField("client_id", provCreds.idpProvisioner.GetIDPCredentialData().GetClientID()). 222 WithField("provider", provCreds.GetIDPProvider().GetName()). 223 Warn("error deprovisioning credential request from IDP, please ask administrator to remove the client from IdP") 224 } 225 } 226 227 ri, _ := cred.AsInstance() 228 h.client.UpdateResourceFinalizer(ri, crFinalizer, "", false) 229 230 // update sub resources when expire 231 if cred.Metadata.State != v1.ResourceDeleting { 232 cred.State.Name = v1.Inactive 233 cred.Status.Level = prov.Success.String() 234 cred.Status.Reasons = []v1.ResourceStatusReason{} 235 h.client.CreateSubResource(cred.ResourceMeta, map[string]interface{}{ 236 "state": cred.State, 237 }) 238 h.client.CreateSubResource(cred.ResourceMeta, map[string]interface{}{ 239 "status": cred.Status, 240 }) 241 } 242 } else { 243 err := fmt.Errorf(status.GetMessage()) 244 logger.WithError(err).Error("request status was not Success, skipping") 245 h.onError(ctx, cred, err) 246 h.client.CreateSubResource(cred.ResourceMeta, cred.SubResources) 247 } 248 } 249 250 func (h *credentials) onPending(ctx context.Context, cred *management.Credential) *management.Credential { 251 // check the application status 252 logger := getLoggerFromContext(ctx) 253 app, crd, shouldReturn := h.provisionPreProcess(ctx, cred) 254 if shouldReturn { 255 return cred 256 } 257 258 provCreds, err := h.newProvCreds(cred, app, nil, 0, crd) 259 if err != nil { 260 logger.WithError(err).Error("error preparing credential request") 261 h.onError(ctx, cred, err) 262 return cred 263 } 264 265 if provCreds.IsIDPCredential() { 266 err := provCreds.idpProvisioner.RegisterClient() 267 if err != nil { 268 logger.WithError(err).Error("error provisioning credential request with IDP") 269 h.onError(ctx, cred, err) 270 return cred 271 } 272 } 273 274 status, credentialData := h.prov.CredentialProvision(provCreds) 275 276 h.provisionPostProcess(status, credentialData, app, crd, provCreds, cred) 277 278 return cred 279 } 280 281 func (h *credentials) provisionPreProcess(ctx context.Context, cred *management.Credential) (*management.ManagedApplication, *management.CredentialRequestDefinition, bool) { 282 logger := getLoggerFromContext(ctx) 283 app, err := h.getManagedApp(ctx, cred) 284 if err != nil { 285 logger.WithError(err).Error("error getting managed app") 286 h.onError(ctx, cred, err) 287 return nil, nil, true 288 } 289 290 if app.Status.Level != prov.Success.String() { 291 err = fmt.Errorf("cannot handle credential when application is not yet successful") 292 h.onError(ctx, cred, err) 293 return nil, nil, true 294 } 295 296 crd, err := h.getCRD(ctx, cred) 297 if err != nil { 298 logger.WithError(err).Error("error getting credential request definition") 299 h.onError(ctx, cred, err) 300 return nil, nil, true 301 } 302 303 return app, crd, false 304 } 305 306 func (h *credentials) provisionPostProcess(status prov.RequestStatus, credentialData prov.Credential, app *management.ManagedApplication, crd *management.CredentialRequestDefinition, provCreds *provCreds, cred *management.Credential) { 307 var err error 308 data := map[string]interface{}{} 309 idpAgentDetails := make(map[string]string) 310 if status.GetStatus() == prov.Success { 311 credentialData := h.getProvisionedCredentialData(provCreds, credentialData) 312 if credentialData != nil { 313 sec := app.Spec.Security 314 d := credentialData.GetData() 315 if crd.Spec.Provision == nil { 316 data = d 317 } else if d != nil { 318 data, err = h.encryptSchema( 319 crd.Spec.Provision.Schema, 320 d, 321 sec.EncryptionKey, sec.EncryptionAlgorithm, sec.EncryptionHash, 322 ) 323 } 324 if provCreds.IsIDPCredential() { 325 idpAgentDetails, err = provCreds.idpProvisioner.GetAgentDetails() 326 } 327 if err != nil { 328 status = prov.NewRequestStatusBuilder(). 329 SetMessage(fmt.Sprintf("error encrypting credential: %s", err.Error())). 330 SetCurrentStatusReasons(cred.Status.Reasons). 331 Failed() 332 } 333 } 334 } 335 336 cred.Data = data 337 cred.Status = prov.NewStatusReason(status) 338 339 // use the expiration time sent back with the data 340 if credentialData != nil && !credentialData.GetExpirationTime().IsZero() { 341 cred.Policies.Expiry = &management.CredentialPoliciesExpiry{ 342 Timestamp: v1.Time(credentialData.GetExpirationTime()), 343 } 344 } else if provCreds.days != 0 { 345 // update the expiration timestamp 346 expTS := time.Now().AddDate(0, 0, provCreds.days) 347 348 cred.Policies.Expiry = &management.CredentialPoliciesExpiry{ 349 Timestamp: v1.Time(expTS), 350 } 351 } 352 353 details := util.MergeMapStringString(util.GetAgentDetailStrings(cred), status.GetProperties(), idpAgentDetails) 354 util.SetAgentDetails(cred, util.MapStringStringToMapStringInterface(details)) 355 356 h.processCredentialLevelSuccess(provCreds, cred) 357 358 cred.SubResources = map[string]interface{}{ 359 defs.XAgentDetails: util.GetAgentDetails(cred), 360 "data": cred.Data, 361 "policies": cred.Policies, 362 "state": cred.State, 363 } 364 } 365 366 func (h *credentials) processCredentialLevelSuccess(provCreds *provCreds, cred *management.Credential) { 367 if cred.Status.Level == prov.Success.String() { 368 if !hasAgentCredentialFinalizer(cred.Finalizers) { 369 ri, _ := cred.AsInstance() 370 // only add finalizer on success 371 h.client.UpdateResourceFinalizer(ri, crFinalizer, "", true) 372 } 373 374 if provCreds.GetCredentialAction() != prov.Rotate { 375 // if this is not a rotate action update the state to the desired state 376 cred.State.Name = cred.Spec.State.Name 377 } else { 378 // if the action was rotate lets remove the rotate flag from spec 379 cred.Spec.State.Rotate = false 380 h.client.UpdateResourceInstance(cred) 381 } 382 } else if cred.State.Name == "" { 383 cred.State.Name = v1.Inactive 384 } 385 } 386 387 func (h *credentials) onUpdates(ctx context.Context, cred *management.Credential, actions []prov.CredentialAction) *management.Credential { 388 logger := getLoggerFromContext(ctx) 389 app, crd, shouldReturn := h.provisionPreProcess(ctx, cred) 390 provData := h.deprovisionPreProcess(ctx, cred) 391 if shouldReturn { 392 return cred 393 } 394 395 for _, action := range actions { 396 provCreds, err := h.newProvCreds(cred, app, provData, action, crd) 397 if err != nil { 398 logger.WithError(err).Error("error preparing credential request") 399 h.onError(ctx, cred, err) 400 return cred 401 } 402 403 if action != prov.Suspend && provCreds.IsIDPCredential() { 404 err := provCreds.idpProvisioner.RegisterClient() 405 if err != nil { 406 logger.WithError(err).Error("error provisioning credential request with IDP") 407 h.onError(ctx, cred, err) 408 return cred 409 } 410 } 411 412 status, credentialData := h.prov.CredentialUpdate(provCreds) 413 h.provisionPostProcess(status, credentialData, app, crd, provCreds, cred) 414 } 415 416 return cred 417 } 418 419 // onError updates the AccessRequest with an error status 420 func (h *credentials) onError(_ context.Context, cred *management.Credential, err error) { 421 ps := prov.NewRequestStatusBuilder() 422 status := ps.SetMessage(fmt.Sprintf("Agent: %s", err.Error())).SetCurrentStatusReasons(cred.Status.Reasons).Failed() 423 cred.Status = prov.NewStatusReason(status) 424 cred.SubResources = map[string]interface{}{ 425 "status": cred.Status, 426 } 427 } 428 429 func (h *credentials) getManagedApp(_ context.Context, cred *management.Credential) (*management.ManagedApplication, error) { 430 app := management.NewManagedApplication(cred.Spec.ManagedApplication, cred.Metadata.Scope.Name) 431 ri, err := h.client.GetResource(app.GetSelfLink()) 432 if err != nil { 433 return nil, err 434 } 435 436 app = &management.ManagedApplication{} 437 err = app.FromInstance(ri) 438 return app, err 439 } 440 441 func (h *credentials) getCRD(_ context.Context, cred *management.Credential) (*management.CredentialRequestDefinition, error) { 442 crd := management.NewCredentialRequestDefinition(cred.Spec.CredentialRequestDefinition, cred.Metadata.Scope.Name) 443 ri, err := h.client.GetResource(crd.GetSelfLink()) 444 if err != nil { 445 return nil, err 446 } 447 448 crd = &management.CredentialRequestDefinition{} 449 err = crd.FromInstance(ri) 450 return crd, err 451 } 452 453 func (h *credentials) getProvisionedCredentialData(provCreds *provCreds, credentialData prov.Credential) prov.Credential { 454 if provCreds.IsIDPCredential() { 455 return prov.NewCredentialBuilder().SetOAuthIDAndSecret( 456 provCreds.GetIDPCredentialData().GetClientID(), 457 provCreds.GetIDPCredentialData().GetClientSecret(), 458 ) 459 } 460 return credentialData 461 } 462 463 func hasAgentCredentialError(status *v1.ResourceStatus) bool { 464 for _, r := range status.Reasons { 465 if strings.HasPrefix(r.Detail, "Agent:") { 466 return true 467 } 468 } 469 return false 470 } 471 472 func hasAgentCredentialFinalizer(finalizers []v1.Finalizer) bool { 473 for _, f := range finalizers { 474 if f.Name == crFinalizer { 475 return true 476 } 477 } 478 return false 479 } 480 481 type provCreds struct { 482 managedApp string 483 credType string 484 id string 485 name string 486 days int 487 credAction prov.CredentialAction 488 credData map[string]interface{} 489 credDetails map[string]interface{} 490 appDetails map[string]interface{} 491 idpProvisioner idp.Provisioner 492 credSchema map[string]interface{} 493 credProvSchema map[string]interface{} 494 credSchemaDetails map[string]interface{} 495 } 496 497 func (h *credentials) newProvCreds(cr *management.Credential, app *management.ManagedApplication, provData map[string]interface{}, action prov.CredentialAction, crd *management.CredentialRequestDefinition) (*provCreds, error) { 498 credDetails := util.GetAgentDetails(cr) 499 500 provCred := &provCreds{ 501 appDetails: util.GetAgentDetails(app), 502 credDetails: credDetails, 503 credType: cr.Spec.CredentialRequestDefinition, 504 credData: cr.Spec.Data, 505 managedApp: cr.Spec.ManagedApplication, 506 id: cr.Metadata.ID, 507 name: cr.Name, 508 credAction: action, 509 days: 0, 510 } 511 512 if crd != nil { 513 if crd.Spec.Provision != nil && 514 crd.Spec.Provision.Policies.Expiry != nil { 515 provCred.days = int(crd.Spec.Provision.Policies.Expiry.Period) 516 } 517 518 credSchemaDetails := util.GetAgentDetails(crd) 519 provCred.credSchema = crd.Spec.Schema 520 if crd.Spec.Provision != nil { 521 provCred.credProvSchema = crd.Spec.Provision.Schema 522 } 523 provCred.credSchemaDetails = credSchemaDetails 524 } 525 idpProvisioner, err := idp.NewProvisioner(context.Background(), h.idpProviderRegistry, app, cr) 526 if err != nil { 527 return nil, fmt.Errorf("IDP provider not found for credential request") 528 } 529 provCred.idpProvisioner = idpProvisioner 530 return provCred, nil 531 } 532 533 // GetApplicationName gets the name of the managed application 534 func (c provCreds) GetApplicationName() string { 535 return c.managedApp 536 } 537 538 // GetID gets the id of the credential resource 539 func (c provCreds) GetID() string { 540 return c.id 541 } 542 543 // GetName gets the name of the credential resource 544 func (c provCreds) GetName() string { 545 return c.name 546 } 547 548 // GetCredentialType gets the type of the credential 549 func (c provCreds) GetCredentialType() string { 550 return c.credType 551 } 552 553 // GetCredentialData gets the data of the credential 554 func (c provCreds) GetCredentialData() map[string]interface{} { 555 return c.credData 556 } 557 558 // GetCredentialAction gets the data of the credential 559 func (c provCreds) GetCredentialAction() prov.CredentialAction { 560 return c.credAction 561 } 562 563 // GetID gets the id of the credential resource 564 func (c provCreds) GetCredentialExpirationDays() int { 565 return c.days 566 } 567 568 // GetCredentialSchema returns the schema for the credential request. 569 func (c provCreds) GetCredentialSchema() map[string]interface{} { 570 return c.credSchema 571 } 572 573 // GetCredentialProvisionSchema returns the provisioning schema for the credential request. 574 func (c provCreds) GetCredentialProvisionSchema() map[string]interface{} { 575 return c.credProvSchema 576 } 577 578 // GetCredentialSchemaDetailsValue returns a value found on the 'x-agent-details' sub resource of the crd. 579 func (c provCreds) GetCredentialSchemaDetailsValue(key string) interface{} { 580 if c.credSchemaDetails == nil { 581 return nil 582 } 583 584 return c.credSchemaDetails[key] 585 } 586 587 // IsIDPCredential returns boolean indicating if the credential request is for IDP provider 588 func (c provCreds) IsIDPCredential() bool { 589 return c.idpProvisioner.IsIDPCredential() 590 } 591 592 // GetIDPProvider returns the interface for IDP provider if the credential request is for IDP provider 593 func (c provCreds) GetIDPProvider() oauth.Provider { 594 return c.idpProvisioner.GetIDPProvider() 595 } 596 597 // GetIDPCredentialData returns the credential data for IDP from the request 598 func (c provCreds) GetIDPCredentialData() prov.IDPCredentialData { 599 return c.idpProvisioner.GetIDPCredentialData() 600 } 601 602 // GetCredentialDetailsValue returns a value found on the 'x-agent-details' sub resource of the Credentials. 603 func (c provCreds) GetCredentialDetailsValue(key string) string { 604 if c.credDetails == nil { 605 return "" 606 } 607 608 return util.ToString(c.credDetails[key]) 609 } 610 611 // GetApplicationDetailsValue returns a value found on the 'x-agent-details' sub resource of the ManagedApplication. 612 func (c provCreds) GetApplicationDetailsValue(key string) string { 613 if c.appDetails == nil { 614 return "" 615 } 616 617 return util.ToString(c.appDetails[key]) 618 } 619 620 // encryptSchema schema is the json schema. credData is the data that contains data to encrypt based on the key, alg and hash. 621 func encryptSchema( 622 schema, credData map[string]interface{}, key, alg, hash string, 623 ) (map[string]interface{}, error) { 624 data := make(map[string]interface{}) 625 enc, err := util.NewEncryptor(key, alg, hash) 626 if err != nil { 627 return data, err 628 } 629 630 schemaProps, ok := schema["properties"] 631 if !ok { 632 return data, fmt.Errorf("properties field not found on schema") 633 } 634 635 props, ok := schemaProps.(map[string]interface{}) 636 if !ok { 637 props = make(map[string]interface{}) 638 } 639 640 return encryptMap(enc, props, credData), nil 641 } 642 643 // encryptMap loops through all data and checks the value against the provisioning schema to see if it should be encrypted. 644 func encryptMap(enc util.Encryptor, schema, data map[string]interface{}) map[string]interface{} { 645 for key, value := range data { 646 schemaValue := schema[key] 647 v, ok := schemaValue.(map[string]interface{}) 648 if !ok { 649 continue 650 } 651 652 if _, ok := v[xAxwayEncrypted]; ok { 653 v, ok := value.(string) 654 if !ok { 655 continue 656 } 657 658 str, err := enc.Encrypt(v) 659 if err != nil { 660 661 log.Error(err) 662 continue 663 } 664 665 data[key] = base64.StdEncoding.EncodeToString([]byte(str)) 666 } 667 } 668 669 return data 670 }