func()

in internal/pkg/api/handleEnroll.go [196:449]


func (et *EnrollerT) _enroll(
	ctx context.Context,
	rb *rollback.Rollback,
	zlog zerolog.Logger,
	req *EnrollRequest,
	policyID string,
	namespaces []string,
	ver string,
) (*EnrollResponse, error) {
	var agent model.Agent
	var enrollmentID string

	span, ctx := apm.StartSpan(ctx, "enroll", "process")
	defer span.End()

	now := time.Now()

	if req.EnrollmentId != nil {
		vSpan, vCtx := apm.StartSpan(ctx, "checkEnrollmentID", "validate")
		enrollmentID = *req.EnrollmentId
		var err error
		agent, err = dl.FindAgent(vCtx, et.bulker, dl.QueryAgentByEnrollmentID, dl.FieldEnrollmentID, enrollmentID)
		if err != nil {
			zlog.Debug().Err(err).
				Str("EnrollmentId", enrollmentID).
				Msg("Agent with EnrollmentId not found")
			if !errors.Is(err, dl.ErrNotFound) && !strings.Contains(err.Error(), "no such index") {
				vSpan.End()
				return nil, err
			}
		}
		vSpan.End()
	}

	// only delete existing agent if it never checked in
	if agent.Id != "" && agent.LastCheckin == "" {
		zlog.Debug().
			Str("EnrollmentId", enrollmentID).
			Str("AgentId", agent.Id).
			Str("APIKeyID", agent.AccessAPIKeyID).
			Msg("Invalidate old api key and remove existing agent with the same enrollment_id")
		// invalidate previous api key
		err := invalidateAPIKey(ctx, zlog, et.bulker, agent.AccessAPIKeyID)
		if err != nil {
			zlog.Error().Err(err).
				Str("EnrollmentId", enrollmentID).
				Str("AgentId", agent.Id).
				Str("APIKeyID", agent.AccessAPIKeyID).
				Msg("Error when trying to invalidate API key of old agent with enrollment id")
			return nil, err
		}
		// delete existing agent to recreate with new api key
		err = deleteAgent(ctx, zlog, et.bulker, agent.Id)
		if err != nil {
			zlog.Error().Err(err).
				Str("EnrollmentId", enrollmentID).
				Str("AgentId", agent.Id).
				Msg("Error when trying to delete old agent with enrollment id")
			return nil, err
		}
		// deleted, so clear the ID so code below knows it needs to be created
		agent.Id = ""
	}

	var agentID string
	if req.Id != nil && *req.Id != "" {
		agentID = *req.Id

		// check if the agent with this ID already exists
		var err error
		agent, err = et._checkAgent(ctx, zlog, agentID)
		if err != nil {
			return nil, err
		}

		if agent.Id != "" {
			// confirm that this agent has a set replace token
			// one is required or replacement of this already enrolled and active
			// agent is not allowed
			if agent.ReplaceToken == "" {
				zlog.Warn().
					Str("AgentId", agent.Id).
					Msg("Existing agent with same ID already enrolled without a replace token set")
				return nil, ErrAgentNotReplaceable
			}
			if req.ReplaceToken == nil || *req.ReplaceToken == "" {
				zlog.Warn().
					Str("AgentId", agent.Id).
					Msg("Existing agent with same ID already enrolled; no replace token given during enrollment")
				return nil, ErrAgentNotReplaceable
			}
			same, err := compareHashAndToken(zlog.With().Str("AgentID", agent.Id).Logger(), agent.ReplaceToken, *req.ReplaceToken, et.cfg.PDKDF2)
			if err != nil {
				// issue with hash comparison; reason already logged
				return nil, ErrAgentNotReplaceable
			}
			if !same {
				// not the same, cannot replace
				// provides no real reason as that would expose too much information
				zlog.Debug().
					Str("AgentId", agent.Id).
					Msg("Existing agent with same ID already enrolled; replace token didn't match")
				return nil, ErrAgentNotReplaceable
			}

			// confirm that its on the same policy
			// it is not supported to have it the same ID enroll into different policies
			if agent.PolicyID != policyID {
				zlog.Warn().
					Str("AgentId", agent.Id).
					Str("PolicyId", policyID).
					Str("CurrentPolicyId", agent.PolicyID).
					Msg("Existing agent with same ID already enrolled into another policy")
				return nil, ErrAgentNotReplaceable
			}

			// invalidate the previous api key
			// this has to be done because it's not possible to get the previous token
			// so the other is invalidated and a new one is generated
			zlog.Debug().
				Str("AgentId", agent.Id).
				Str("APIKeyID", agent.AccessAPIKeyID).
				Msg("Invalidate old api key with same id")
			err = invalidateAPIKey(ctx, zlog, et.bulker, agent.AccessAPIKeyID)
			if err != nil {
				zlog.Error().Err(err).
					Str("AgentId", agent.Id).
					Str("APIKeyID", agent.AccessAPIKeyID).
					Msg("Error when trying to invalidate API key of old agent with same id")
				return nil, err
			}
		}
	} else {
		// No ID provided so generate an ID.
		u, err := uuid.NewV4()
		if err != nil {
			return nil, err
		}
		agentID = u.String()
	}

	// Update the local metadata agent id
	localMeta, err := updateLocalMetaAgentID(req.Metadata.Local, agentID)
	if err != nil {
		return nil, err
	}

	// Generate the Fleet Agent access api key
	accessAPIKey, err := generateAccessAPIKey(ctx, et.bulker, agentID)
	if err != nil {
		return nil, err
	}

	// Register invalidate API key function for enrollment error rollback
	rb.Register("invalidate API key", func(ctx context.Context) error {
		return invalidateAPIKey(ctx, zlog, et.bulker, accessAPIKey.ID)
	})

	// Existing agent, only update a subset of the fields
	if agent.Id != "" {
		agent.Active = true
		agent.Namespaces = namespaces
		agent.LocalMetadata = localMeta
		agent.AccessAPIKeyID = accessAPIKey.ID
		agent.Agent = &model.AgentMetadata{
			ID:      agentID,
			Version: ver,
		}
		agent.Tags = removeDuplicateStr(req.Metadata.Tags)
		agentField, err := json.Marshal(agent.Agent)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal agent to JSON: %w", err)
		}
		// update the agent record
		// clears state of policy revision, as this agent needs to get the latest policy
		// clears state of unenrollment, as this is a new enrollment
		doc := bulk.UpdateFields{
			dl.FieldNamespaces:            namespaces,
			dl.FieldLocalMetadata:         json.RawMessage(localMeta),
			dl.FieldAccessAPIKeyID:        accessAPIKey.ID,
			dl.FieldAgent:                 json.RawMessage(agentField),
			dl.FieldTags:                  agent.Tags,
			dl.FieldPolicyRevisionIdx:     0,
			dl.FieldAuditUnenrolledTime:   nil,
			dl.FieldAuditUnenrolledReason: nil,
			dl.FieldUnenrolledAt:          nil,
			dl.FieldUnenrolledReason:      nil,
			dl.FieldUpdatedAt:             now.UTC().Format(time.RFC3339),
		}
		err = updateFleetAgent(ctx, et.bulker, agentID, doc)
		if err != nil {
			return nil, err
		}
	} else {
		var replaceHash string
		if req.ReplaceToken != nil && *req.ReplaceToken != "" {
			var err error
			replaceHash, err = hashReplaceToken(*req.ReplaceToken, et.cfg.PDKDF2)
			if err != nil {
				zlog.Error().Err(err).Msg("failed generate hash of replace token")
				return nil, err
			}
		}

		agent = model.Agent{
			Active:         true,
			PolicyID:       policyID,
			Namespaces:     namespaces,
			Type:           string(req.Type),
			EnrolledAt:     now.UTC().Format(time.RFC3339),
			LocalMetadata:  localMeta,
			AccessAPIKeyID: accessAPIKey.ID,
			ActionSeqNo:    []int64{sqn.UndefinedSeqNo},
			Agent: &model.AgentMetadata{
				ID:      agentID,
				Version: ver,
			},
			Tags:         removeDuplicateStr(req.Metadata.Tags),
			EnrollmentID: enrollmentID,
			ReplaceToken: replaceHash,
		}

		err = createFleetAgent(ctx, et.bulker, agentID, agent)
		if err != nil {
			return nil, err
		}
		// Register delete fleet agent for enrollment error rollback
		rb.Register("delete agent", func(ctx context.Context) error {
			return deleteAgent(ctx, zlog, et.bulker, agentID)
		})
	}

	resp := EnrollResponse{
		Action: "created",
		Item: EnrollResponseItem{
			AccessApiKey:         accessAPIKey.Token(),
			AccessApiKeyId:       agent.AccessAPIKeyID,
			Active:               agent.Active,
			EnrolledAt:           agent.EnrolledAt,
			Id:                   agentID,
			LocalMetadata:        agent.LocalMetadata,
			PolicyId:             agent.PolicyID,
			Status:               "online",
			Tags:                 agent.Tags,
			Type:                 agent.Type,
			UserProvidedMetadata: agent.UserProvidedMetadata,
		},
	}

	// We are Kool & and the Gang; cache the access key to avoid the roundtrip on impending checkin
	et.cache.SetAPIKey(*accessAPIKey, true)

	return &resp, nil
}