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
}