Add backend support for Smallstep CA (#32872)
Co-authored-by: Jordan Montgomery <elijah.jordan.montgomery@gmail.com> Co-authored-by: Magnus Jensen <magnus@fleetdm.com>
This commit is contained in:
co-authored by
Jordan Montgomery
Magnus Jensen
parent
d20ade7947
commit
128a71eb4f
@@ -59,9 +59,7 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
||||
caDisplayType := "Unknown"
|
||||
|
||||
if p.DigiCert != nil {
|
||||
p.DigiCert.Name = fleet.Preprocess(p.DigiCert.Name)
|
||||
p.DigiCert.URL = fleet.Preprocess(p.DigiCert.URL)
|
||||
p.DigiCert.ProfileID = fleet.Preprocess(p.DigiCert.ProfileID)
|
||||
p.DigiCert.Preprocess()
|
||||
if err := svc.validateDigicert(ctx, p.DigiCert, errPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -78,8 +76,8 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
||||
}
|
||||
|
||||
if p.Hydrant != nil {
|
||||
p.Hydrant.Name = fleet.Preprocess(p.Hydrant.Name)
|
||||
p.Hydrant.URL = fleet.Preprocess(p.Hydrant.URL)
|
||||
p.Hydrant.Preprocess()
|
||||
|
||||
if err := svc.validateHydrant(ctx, p.Hydrant, errPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,9 +92,8 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
||||
}
|
||||
|
||||
if p.NDESSCEPProxy != nil {
|
||||
p.NDESSCEPProxy.URL = fleet.Preprocess(p.NDESSCEPProxy.URL)
|
||||
p.NDESSCEPProxy.AdminURL = fleet.Preprocess(p.NDESSCEPProxy.AdminURL)
|
||||
p.NDESSCEPProxy.Username = fleet.Preprocess(p.NDESSCEPProxy.Username)
|
||||
p.NDESSCEPProxy.Preprocess()
|
||||
|
||||
if err := svc.validateNDESSCEPProxy(ctx, p.NDESSCEPProxy, errPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,8 +109,7 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
||||
}
|
||||
|
||||
if p.CustomSCEPProxy != nil {
|
||||
p.CustomSCEPProxy.Name = fleet.Preprocess(p.CustomSCEPProxy.Name)
|
||||
p.CustomSCEPProxy.URL = fleet.Preprocess(p.CustomSCEPProxy.URL)
|
||||
p.CustomSCEPProxy.Preprocess()
|
||||
|
||||
if err := svc.validateCustomSCEPProxy(ctx, p.CustomSCEPProxy, errPrefix); err != nil {
|
||||
return nil, err
|
||||
@@ -127,6 +123,23 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
||||
activity = fleet.ActivityAddedCustomSCEPProxy{Name: p.CustomSCEPProxy.Name}
|
||||
}
|
||||
|
||||
if p.Smallstep != nil {
|
||||
p.Smallstep.Preprocess()
|
||||
|
||||
if err := svc.validateSmallstepSCEPProxy(ctx, p.Smallstep, errPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caToCreate.Type = string(fleet.CATypeSmallstep)
|
||||
caToCreate.Name = &p.Smallstep.Name
|
||||
caToCreate.URL = &p.Smallstep.URL
|
||||
caToCreate.ChallengeURL = &p.Smallstep.ChallengeURL
|
||||
caToCreate.Username = &p.Smallstep.Username
|
||||
caToCreate.Password = &p.Smallstep.Password
|
||||
caDisplayType = "Smallstep"
|
||||
activity = fleet.ActivityAddedSmallstep{Name: p.Smallstep.Name}
|
||||
}
|
||||
|
||||
createdCA, err := svc.ds.NewCertificateAuthority(ctx, caToCreate)
|
||||
if err != nil {
|
||||
if errors.As(err, &fleet.ConflictError{}) {
|
||||
@@ -159,6 +172,9 @@ func (svc *Service) validatePayload(p *fleet.CertificateAuthorityPayload, errPre
|
||||
if p.CustomSCEPProxy != nil {
|
||||
casToCreate++
|
||||
}
|
||||
if p.Smallstep != nil {
|
||||
casToCreate++
|
||||
}
|
||||
if casToCreate == 0 {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sA certificate authority must be specified", errPrefix)}
|
||||
}
|
||||
@@ -349,6 +365,30 @@ func (svc *Service) validateCustomSCEPProxy(ctx context.Context, customSCEP *fle
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateSmallstepSCEPProxy(ctx context.Context, smallstepSCEP *fleet.SmallstepSCEPProxyCA, errPrefix string) error {
|
||||
if err := validateCAName(smallstepSCEP.Name, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateURL(smallstepSCEP.URL, "Smallstep SCEP", errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if smallstepSCEP.Username == "" {
|
||||
return fleet.NewInvalidArgumentError("username", fmt.Sprintf("%sSmallstep username cannot be empty", errPrefix))
|
||||
}
|
||||
if smallstepSCEP.Password == "" || smallstepSCEP.Password == fleet.MaskedPassword {
|
||||
return fleet.NewInvalidArgumentError("password", fmt.Sprintf("%sSmallstep password cannot be empty", errPrefix))
|
||||
}
|
||||
if err := svc.scepConfigService.ValidateSCEPURL(ctx, smallstepSCEP.URL); err != nil {
|
||||
level.Error(svc.logger).Log("msg", "Failed to validate Smallstep SCEP URL", "err", err)
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid SCEP URL. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
if err := svc.scepConfigService.ValidateSmallstepChallengeURL(ctx, *smallstepSCEP); err != nil {
|
||||
level.Error(svc.logger).Log("msg", "Failed to validate Smallstep SCEP admin URL", "err", err)
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid challenge URL or credentials. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type oauthIntrospectionResponse struct {
|
||||
Username *string `json:"username"`
|
||||
// Only active is required in the body by the spec
|
||||
@@ -381,6 +421,10 @@ func (svc *Service) DeleteCertificateAuthority(ctx context.Context, certificateA
|
||||
activity = fleet.ActivityDeletedHydrant{
|
||||
Name: ca.Name,
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
activity = fleet.ActivityDeletedSmallstep{
|
||||
Name: ca.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), activity); err != nil {
|
||||
@@ -456,9 +500,8 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
||||
if ca.Name == "" {
|
||||
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.digicert: CA name cannot be empty.")
|
||||
}
|
||||
ca.Name = fleet.Preprocess(ca.Name)
|
||||
ca.URL = fleet.Preprocess(ca.URL)
|
||||
ca.ProfileID = fleet.Preprocess(ca.ProfileID)
|
||||
ca.Preprocess()
|
||||
|
||||
if err := checkAllNames(ca.Name, "digicert", "DigiCert"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -468,7 +511,8 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
||||
if ca.Name == "" {
|
||||
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.custom_scep_proxy: CA name cannot be empty.")
|
||||
}
|
||||
ca.Name = fleet.Preprocess(ca.Name)
|
||||
ca.Preprocess()
|
||||
|
||||
if err := checkAllNames(ca.Name, "custom_scep_proxy", "Custom SCEP Proxy"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -478,17 +522,26 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
||||
if ca.Name == "" {
|
||||
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.hydrant: CA name cannot be empty.")
|
||||
}
|
||||
ca.Name = fleet.Preprocess(ca.Name)
|
||||
ca.URL = fleet.Preprocess(ca.URL)
|
||||
ca.Preprocess()
|
||||
|
||||
if err := checkAllNames(ca.Name, "hydrant", "Hydrant"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// preprocess smallstep
|
||||
for _, ca := range incoming.Smallstep {
|
||||
if ca.Name == "" {
|
||||
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.smallstep: CA name cannot be empty.")
|
||||
}
|
||||
ca.Preprocess()
|
||||
|
||||
if err := checkAllNames(ca.Name, "smallstep", "Smallstep"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// preprocess ndes
|
||||
if incoming.NDESSCEP != nil {
|
||||
incoming.NDESSCEP.URL = fleet.Preprocess(incoming.NDESSCEP.URL)
|
||||
incoming.NDESSCEP.AdminURL = fleet.Preprocess(incoming.NDESSCEP.AdminURL)
|
||||
incoming.NDESSCEP.Username = fleet.Preprocess(incoming.NDESSCEP.Username)
|
||||
incoming.NDESSCEP.Preprocess()
|
||||
}
|
||||
|
||||
if err := svc.processNDESSCEP(ctx, batchOps, incoming.NDESSCEP, existing.NDESSCEP); err != nil {
|
||||
@@ -503,6 +556,9 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
||||
if err := svc.processHydrantCAs(ctx, batchOps, incoming.Hydrant, existing.Hydrant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.processSmallstepCAs(ctx, batchOps, incoming.Smallstep, existing.Smallstep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return batchOps, nil
|
||||
}
|
||||
@@ -744,6 +800,62 @@ func (svc *Service) processHydrantCAs(ctx context.Context, batchOps *fleet.Certi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) processSmallstepCAs(ctx context.Context, batchOps *fleet.CertificateAuthoritiesBatchOperations, incomingCAs []fleet.SmallstepSCEPProxyCA, existingCAs []fleet.SmallstepSCEPProxyCA) error {
|
||||
incomingByName := make(map[string]*fleet.SmallstepSCEPProxyCA)
|
||||
for _, incoming := range incomingCAs {
|
||||
// Note: caller is responsible for ensuring incoming list has no duplicates
|
||||
incomingByName[incoming.Name] = &incoming
|
||||
}
|
||||
|
||||
existingByName := make(map[string]*fleet.SmallstepSCEPProxyCA)
|
||||
for _, existing := range existingCAs {
|
||||
// if existing CA isn't in the incoming list, we should delete it
|
||||
if _, ok := incomingByName[existing.Name]; !ok {
|
||||
batchOps.Delete = append(batchOps.Delete, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeSmallstep),
|
||||
Name: &existing.Name,
|
||||
URL: &existing.URL,
|
||||
ChallengeURL: &existing.ChallengeURL,
|
||||
Username: &existing.Username,
|
||||
Password: &existing.Password,
|
||||
})
|
||||
}
|
||||
// Note: datastore is responsible for ensuring no existing list has no duplicates
|
||||
existingByName[existing.Name] = &existing
|
||||
}
|
||||
|
||||
for name, incoming := range incomingByName {
|
||||
if err := svc.validateSmallstepSCEPProxy(ctx, incoming, "certificate_authorities.smallstep: "); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the payload to be added or updated
|
||||
if _, ok := existingByName[name]; ok {
|
||||
// update existing
|
||||
batchOps.Update = append(batchOps.Update, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeSmallstep),
|
||||
Name: &incoming.Name,
|
||||
URL: &incoming.URL,
|
||||
ChallengeURL: &incoming.ChallengeURL,
|
||||
Username: &incoming.Username,
|
||||
Password: &incoming.Password,
|
||||
})
|
||||
} else {
|
||||
// add new
|
||||
batchOps.Add = append(batchOps.Add, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeSmallstep),
|
||||
Name: &incoming.Name,
|
||||
URL: &incoming.URL,
|
||||
ChallengeURL: &incoming.ChallengeURL,
|
||||
Username: &incoming.Username,
|
||||
Password: &incoming.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordActivitiesBatchApplyCAs records activities for batch operations on certificate authorities
|
||||
// (i.e. added, edited, deleted).
|
||||
func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *fleet.CertificateAuthoritiesBatchOperations) error {
|
||||
@@ -769,6 +881,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for added hydrant")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for added smallstep SCEP proxy")
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ca := range ops.Update {
|
||||
@@ -789,6 +905,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for edited hydrant")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for edited smallstep SCEP proxy")
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ca := range ops.Delete {
|
||||
@@ -809,6 +929,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for deleted hydrant")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for deleted smallstep SCEP proxy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,6 +1062,32 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
||||
activity = fleet.ActivityEditedCustomSCEPProxy{Name: caActivityName}
|
||||
|
||||
}
|
||||
if p.SmallstepSCEPProxyCAUpdatePayload != nil {
|
||||
if p.SmallstepSCEPProxyCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sSmallstep SCEP Proxy CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
||||
if err := p.SmallstepSCEPProxyCAUpdatePayload.ValidateRelatedFields(errPrefix, *oldCA.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
p.SmallstepSCEPProxyCAUpdatePayload.Preprocess()
|
||||
if err := svc.validateSmallstepSCEPProxyUpdate(ctx, p.SmallstepSCEPProxyCAUpdatePayload, oldCA, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caToUpdate.Type = string(fleet.CATypeSmallstep)
|
||||
caToUpdate.Name = p.SmallstepSCEPProxyCAUpdatePayload.Name
|
||||
caToUpdate.URL = p.SmallstepSCEPProxyCAUpdatePayload.URL
|
||||
caToUpdate.ChallengeURL = p.SmallstepSCEPProxyCAUpdatePayload.ChallengeURL
|
||||
caToUpdate.Username = p.SmallstepSCEPProxyCAUpdatePayload.Username
|
||||
caToUpdate.Password = p.SmallstepSCEPProxyCAUpdatePayload.Password
|
||||
if caToUpdate.Name != nil {
|
||||
caActivityName = *caToUpdate.Name
|
||||
} else {
|
||||
caActivityName = *oldCA.Name
|
||||
}
|
||||
activity = fleet.ActivityEditedSmallstep{Name: caActivityName}
|
||||
}
|
||||
|
||||
if oldCA.Type != caToUpdate.Type {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sThe certificate authority types must be the same.", errPrefix)}
|
||||
@@ -1146,6 +1296,65 @@ func (svc *Service) validateCustomSCEPProxyUpdate(ctx context.Context, customSCE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateSmallstepSCEPProxyUpdate(ctx context.Context, smallstep *fleet.SmallstepSCEPProxyCAUpdatePayload, oldCa *fleet.CertificateAuthority, errPrefix string) error {
|
||||
if smallstep.Name != nil {
|
||||
if err := validateCAName(*smallstep.Name, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if smallstep.URL != nil {
|
||||
if err := validateURL(*smallstep.URL, "SCEP", errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.scepConfigService.ValidateSCEPURL(ctx, *smallstep.URL); err != nil {
|
||||
level.Error(svc.logger).Log("msg", "Failed to validate Smallstep SCEP URL", "err", err)
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid SCEP URL. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
}
|
||||
if smallstep.ChallengeURL != nil {
|
||||
if err := validateURL(*smallstep.ChallengeURL, "Challenge", errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want to generate a SmallsteSCEPProxyCA struct with all required fields to verify the admin URL.
|
||||
// If URL, Username or Password are not being updated we use the existing values from oldCA
|
||||
smallstepSCEPProxy := fleet.SmallstepSCEPProxyCA{
|
||||
ChallengeURL: *smallstep.ChallengeURL,
|
||||
}
|
||||
if smallstep.URL != nil {
|
||||
smallstepSCEPProxy.URL = *smallstep.URL
|
||||
} else {
|
||||
smallstepSCEPProxy.URL = *oldCa.URL
|
||||
}
|
||||
if smallstep.Username != nil {
|
||||
smallstepSCEPProxy.Username = *smallstep.Username
|
||||
} else {
|
||||
smallstepSCEPProxy.Username = *oldCa.Username
|
||||
}
|
||||
if smallstep.Password != nil {
|
||||
smallstepSCEPProxy.Password = *smallstep.Password
|
||||
} else {
|
||||
smallstepSCEPProxy.Password = *oldCa.Password
|
||||
}
|
||||
|
||||
if err := svc.scepConfigService.ValidateSmallstepChallengeURL(ctx, smallstepSCEPProxy); err != nil {
|
||||
level.Error(svc.logger).Log("msg", "Failed to validate Smallstep challenge URL", "err", err)
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid challenge URL or credentials. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
}
|
||||
if smallstep.Username != nil && *smallstep.Username == "" {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("%sSmallstep SCEP Proxy username cannot be empty", errPrefix),
|
||||
}
|
||||
}
|
||||
if smallstep.Password != nil && *smallstep.Password == "" {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("%sSmallstep SCEP Proxy password cannot be empty", errPrefix),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtDuplicateCANameError(name, caType, displayCAType string) error {
|
||||
return fleet.NewInvalidArgumentError("name", fmt.Sprintf("certificate_authorities.%s.name: Couldn’t edit certificate authority. "+
|
||||
"\"%s\" name is already used by another %s certificate authority. Please choose a different name and try again.", caType, name, displayCAType))
|
||||
|
||||
@@ -169,6 +169,13 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
||||
}
|
||||
if ca.Type != string(fleet.CATypeNDESSCEPProxy) {
|
||||
assert.Nil(t, ca.AdminURL)
|
||||
}
|
||||
if ca.Type != string(fleet.CATypeSmallstep) {
|
||||
assert.Nil(t, ca.ChallengeURL)
|
||||
}
|
||||
|
||||
// Since username and password is now shared for NDES and Smallstep
|
||||
if ca.Type != string(fleet.CATypeNDESSCEPProxy) && ca.Type != string(fleet.CATypeSmallstep) {
|
||||
assert.Nil(t, ca.Username)
|
||||
assert.Nil(t, ca.Password)
|
||||
}
|
||||
@@ -199,8 +206,9 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
||||
digiCertService: digicert.NewService(),
|
||||
hydrantService: hydrant.NewService(),
|
||||
scepConfigService: &scep_mock.SCEPConfigService{
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
|
||||
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error { return nil },
|
||||
},
|
||||
}
|
||||
svc.config.Server.PrivateKey = "supersecret"
|
||||
@@ -210,6 +218,27 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
||||
return svc, ctx
|
||||
}
|
||||
|
||||
t.Run("Errors when no CA type is specified", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createRequest := fleet.CertificateAuthorityPayload{}
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createRequest)
|
||||
require.EqualError(t, err, "Couldn't add certificate authority. A certificate authority must be specified")
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Errors when multiple CA types are specified", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
createRequest := fleet.CertificateAuthorityPayload{
|
||||
DigiCert: &fleet.DigiCertCA{},
|
||||
Hydrant: &fleet.HydrantCA{},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createRequest)
|
||||
require.EqualError(t, err, "Couldn't add certificate authority. Only one certificate authority can be created at a time")
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create DigiCert CA - Happy path", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
@@ -395,6 +424,36 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
||||
verifyNilFieldsForType(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - Happy path", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.EqualError(t, err, "mock error to avoid NewActivity panic")
|
||||
require.Len(t, createdCAs, 1)
|
||||
createdCA := createdCAs[0]
|
||||
|
||||
assert.Equal(t, createSmallstepRequest.Smallstep.Name, *createdCA.Name)
|
||||
assert.Equal(t, createSmallstepRequest.Smallstep.URL, *createdCA.URL)
|
||||
assert.Equal(t, string(fleet.CATypeSmallstep), createdCA.Type)
|
||||
require.NotNil(t, createdCA.ChallengeURL)
|
||||
assert.Equal(t, createSmallstepRequest.Smallstep.ChallengeURL, *createdCA.ChallengeURL)
|
||||
require.NotNil(t, createdCA.Username)
|
||||
assert.Equal(t, createSmallstepRequest.Smallstep.Username, *createdCA.Username)
|
||||
require.NotNil(t, createdCA.Password)
|
||||
assert.Equal(t, createSmallstepRequest.Smallstep.Password, *createdCA.Password)
|
||||
verifyNilFieldsForType(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create DigiCert CA - Bad Name", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
@@ -886,6 +945,150 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - bad name", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "Smallstep SCEP WIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Invalid characters in the \"name\" field.")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - invalid URL format", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "bozo",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Invalid Smallstep SCEP URL.")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - empty username", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Smallstep username cannot be empty")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - empty password", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: "",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Smallstep password cannot be empty")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - masked password", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: fleet.MaskedPassword,
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Smallstep password cannot be empty")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - invalid SCEP URL", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
svc.scepConfigService = &scep_mock.SCEPConfigService{
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
|
||||
}
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "https://smallstep.example.com/challenge",
|
||||
Username: "smallstep_user",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Invalid SCEP URL. Please correct and try again.")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
|
||||
t.Run("Create Smallstep SCEP CA - invalid challenge validation", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
svc.scepConfigService = &scep_mock.SCEPConfigService{
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error {
|
||||
return errors.New("some error")
|
||||
},
|
||||
}
|
||||
|
||||
createSmallstepRequest := fleet.CertificateAuthorityPayload{
|
||||
Smallstep: &fleet.SmallstepSCEPProxyCA{
|
||||
Name: "SmallstepSCEPWIFI",
|
||||
URL: "https://smallstep.example.com",
|
||||
ChallengeURL: "bozo",
|
||||
Username: "smallstep_user",
|
||||
Password: "smallstep_password",
|
||||
},
|
||||
}
|
||||
|
||||
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
|
||||
require.ErrorContains(t, err, "Invalid challenge URL or credentials.")
|
||||
require.Len(t, createdCAs, 0)
|
||||
require.Nil(t, createdCA)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
@@ -903,6 +1106,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
hydrantID := uint(2)
|
||||
scepID := uint(3)
|
||||
ndesID := uint(4)
|
||||
smallstepID := uint(5)
|
||||
createdCAs := []*fleet.CertificateAuthority{}
|
||||
baseSetupForCATests := func() (*Service, context.Context) {
|
||||
ds := new(mock.Store)
|
||||
@@ -950,7 +1154,17 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
Password: ptr.String("ndes-password"),
|
||||
}
|
||||
|
||||
createdCAs = append(createdCAs, digicertCA, hydrantCA, customSCEPCA, ndesCA)
|
||||
smallstepCA := &fleet.CertificateAuthority{
|
||||
ID: smallstepID,
|
||||
Name: ptr.String("Smallstep CA"),
|
||||
URL: ptr.String("https://smallstep.example.com"),
|
||||
Type: string(fleet.CATypeSmallstep),
|
||||
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
|
||||
Username: ptr.String("smallstep-username"),
|
||||
Password: ptr.String("smallstep-password"),
|
||||
}
|
||||
|
||||
createdCAs = append(createdCAs, digicertCA, hydrantCA, customSCEPCA, ndesCA, smallstepCA)
|
||||
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
|
||||
for _, ca := range createdCAs {
|
||||
if ca.ID == id {
|
||||
@@ -987,8 +1201,9 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
digiCertService: digicert.NewService(),
|
||||
hydrantService: hydrant.NewService(),
|
||||
scepConfigService: &scep_mock.SCEPConfigService{
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
|
||||
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error { return nil },
|
||||
},
|
||||
}
|
||||
svc.config.Server.PrivateKey = "supersecret"
|
||||
@@ -998,6 +1213,29 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
return svc, ctx
|
||||
}
|
||||
|
||||
t.Run("Errors on empty payload", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, digicertID, fleet.CertificateAuthorityUpdatePayload{})
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. A certificate authority must be specified")
|
||||
})
|
||||
|
||||
t.Run("Errors on multiple payloads", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
|
||||
APIToken: ptr.String("updated-api-token"),
|
||||
},
|
||||
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
|
||||
ClientSecret: ptr.String("updated-secret"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. Only one certificate authority can be edited at a time")
|
||||
})
|
||||
|
||||
t.Run("Errors if no certificate authority is found", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
@@ -1024,6 +1262,9 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
ndesID: {
|
||||
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{},
|
||||
},
|
||||
smallstepID: {
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{},
|
||||
},
|
||||
}
|
||||
|
||||
for id, payload := range payloadMap {
|
||||
@@ -1524,10 +1765,157 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. Insufficient permissions for NDES SCEP admin URL. Please correct and try again.")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Smallstep SCEP", func(t *testing.T) {
|
||||
t.Run("Full update succeeds", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
Name: ptr.String("Updated_Smallstep"),
|
||||
URL: ptr.String("https://smallstep.example.com"),
|
||||
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
|
||||
Username: ptr.String("smallstep_user"),
|
||||
Password: ptr.String("smallstep_password"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "mock error to avoid NewActivity panic")
|
||||
})
|
||||
|
||||
t.Run("Bad name", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
Name: ptr.String("Updated Smallstep"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "validation failed: name Couldn't edit certificate authority. Invalid characters in the \"name\" field. Only letters, numbers and underscores allowed.")
|
||||
})
|
||||
|
||||
t.Run("Invalid URL format", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
URL: ptr.String("bozo"),
|
||||
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
|
||||
Username: ptr.String("updated-username"),
|
||||
Password: ptr.String("updated-password"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid SCEP URL. Please correct and try again.")
|
||||
})
|
||||
|
||||
t.Run("Invalid Challenge URL format", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
URL: ptr.String("https://smallstep.example.com"),
|
||||
ChallengeURL: ptr.String("bozo"),
|
||||
Username: ptr.String("updated-username"),
|
||||
Password: ptr.String("updated-password"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid Challenge URL. Please correct and try again.")
|
||||
})
|
||||
|
||||
t.Run("Bad Smallstep SCEP URL", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
|
||||
svc.scepConfigService = &scep_mock.SCEPConfigService{
|
||||
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
|
||||
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error {
|
||||
return errors.New("some error")
|
||||
},
|
||||
}
|
||||
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
URL: ptr.String("https://smallstep.example.com"),
|
||||
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
|
||||
Username: ptr.String("updated-username"),
|
||||
Password: ptr.String("updated-password"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid challenge URL or credentials. Please correct and try again.")
|
||||
})
|
||||
|
||||
t.Run("Requires all fields when updating URL", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
URL: ptr.String("https://smallstep.example.com"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. \"challenge_url\", \"username\" and \"password\" must be set when modifying \"url\" of an existing certificate authority: Smallstep CA.")
|
||||
})
|
||||
|
||||
t.Run("Requires password and username when updating challenge URL", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. \"username\" and \"password\" must be set when modifying \"challenge_url\" of an existing certificate authority: Smallstep CA.")
|
||||
})
|
||||
|
||||
t.Run("Requires password when updating username", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
Username: ptr.String("updated-username"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. \"password\" must be set when modifying \"username\" of an existing certificate authority: Smallstep CA.")
|
||||
})
|
||||
|
||||
t.Run("Errors on empty username", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
Username: ptr.String(""),
|
||||
Password: ptr.String("updated-password"),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. Smallstep SCEP Proxy username cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("Errors on empty password", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForCATests()
|
||||
payload := fleet.CertificateAuthorityUpdatePayload{
|
||||
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
|
||||
Username: ptr.String("updated-username"),
|
||||
Password: ptr.String(""),
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
|
||||
require.EqualError(t, err, "Couldn't edit certificate authority. Smallstep SCEP Proxy password cannot be empty")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Revisit this test, as it seems rather useless (at least the success case) due to it's simplicity
|
||||
// and not being possible atm. to mock/call free service methods.
|
||||
func TestDeleteCertificateAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
@@ -29,10 +32,11 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
fullPasswordCache = "The password cache is full."
|
||||
ndesInsufficientPermissions = "You do not have sufficient permission to enroll with SCEP."
|
||||
MessageSCEPProxyNotConfigured = "SCEP proxy is not configured"
|
||||
NDESChallengeInvalidAfter = 57 * time.Minute
|
||||
fullPasswordCache = "The password cache is full."
|
||||
ndesInsufficientPermissions = "You do not have sufficient permission to enroll with SCEP."
|
||||
MessageSCEPProxyNotConfigured = "SCEP proxy is not configured"
|
||||
NDESChallengeInvalidAfter = 57 * time.Minute
|
||||
SmallstepChallengeInvalidAfter = 57 * time.Minute // TODO(sca): confirm expected expiration time for smallstep
|
||||
)
|
||||
|
||||
type scepProxyService struct {
|
||||
@@ -164,6 +168,7 @@ func (svc *scepProxyService) validateIdentifier(ctx context.Context, identifier
|
||||
hostUUID, profileUUID)}
|
||||
}
|
||||
var scepURL string
|
||||
|
||||
switch profile.Type {
|
||||
case fleet.CAConfigNDES:
|
||||
if groupedCAs.NDESSCEP == nil {
|
||||
@@ -180,6 +185,31 @@ func (svc *scepProxyService) validateIdentifier(ctx context.Context, identifier
|
||||
return "", &scepserver.BadRequestError{Message: "challenge password has expired"}
|
||||
}
|
||||
scepURL = groupedCAs.NDESSCEP.URL
|
||||
|
||||
case fleet.CAConfigSmallstep:
|
||||
if len(groupedCAs.Smallstep) < 1 {
|
||||
return "", &scepserver.BadRequestError{Message: MessageSCEPProxyNotConfigured}
|
||||
}
|
||||
for _, ca := range groupedCAs.Smallstep {
|
||||
if ca.Name == profile.CAName {
|
||||
scepURL = ca.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
// TODO(sca): confirm if this resend method works for smallstep or if we need to use
|
||||
// something like the approach taken for custom SCEP profiles (where we blank the command uuid
|
||||
// to force a regeneration of the command bytes)
|
||||
// Also confirm the expected expiration time for smallstep challenges
|
||||
if checkChallenge && profile.ChallengeRetrievedAt != nil && profile.ChallengeRetrievedAt.Add(NDESChallengeInvalidAfter).Before(time.Now()) {
|
||||
// The challenge password was retrieved for this profile, and is now invalid.
|
||||
// We need to resend the profile with a new challenge password.
|
||||
// Note: we don't actually know if it is invalid, and we can't get that exact feedback from SCEP server.
|
||||
if err = svc.ds.ResendHostMDMProfile(ctx, hostUUID, profileUUID); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "resending host mdm profile")
|
||||
}
|
||||
return "", &scepserver.BadRequestError{Message: "challenge password has expired"}
|
||||
}
|
||||
|
||||
case fleet.CAConfigCustomSCEPProxy:
|
||||
if len(groupedCAs.CustomScepProxy) < 1 {
|
||||
return "", &scepserver.BadRequestError{Message: MessageSCEPProxyNotConfigured}
|
||||
@@ -342,3 +372,52 @@ func (s *SCEPConfigService) ValidateSCEPURL(ctx context.Context, url string) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SCEPConfigService) ValidateSmallstepChallengeURL(ctx context.Context, ca fleet.SmallstepSCEPProxyCA) error {
|
||||
_, err := s.GetSmallstepSCEPChallenge(ctx, ca)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SCEPConfigService) GetSmallstepSCEPChallenge(ctx context.Context, ca fleet.SmallstepSCEPProxyCA) (string, error) {
|
||||
// Get the challenge from Smallstep
|
||||
client := fleethttp.NewClient(fleethttp.WithTimeout(30 * time.Second))
|
||||
client.Transport = ntlmssp.Negotiator{
|
||||
RoundTripper: fleethttp.NewTransport(),
|
||||
}
|
||||
var reqBody bytes.Buffer
|
||||
if err := json.NewEncoder(&reqBody).Encode(fleet.SmallstepChallengeRequestBody{
|
||||
Webhook: fleet.SmallstepChallengeWebhook{
|
||||
ID: 1,
|
||||
WebhookEvent: "SCEPChallenge",
|
||||
EventTimestamp: time.Now().Unix(),
|
||||
Name: "SCEPChallenge",
|
||||
},
|
||||
Event: fleet.SmallstepChallengeEvent{
|
||||
SCEPServerURL: ca.URL,
|
||||
PayloadIdentifier: uuid.New().String(),
|
||||
PayloadTypes: []string{"com.apple.security.scep"},
|
||||
},
|
||||
}); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "encoding params as JSON")
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, ca.ChallengeURL, &reqBody)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "creating request")
|
||||
}
|
||||
req.SetBasicAuth(ca.Username, ca.Password)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "sending request")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", ctxerr.Wrap(ctx, fmt.Errorf("status code %d", resp.StatusCode), "getting Smallstep SCEP challenge")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "reading response body")
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestValidateNDESSCEPAdminURL(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateNDESSCEPURL(t *testing.T) {
|
||||
func TestValidateSCEPURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := NewTestSCEPServer(t)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -133,6 +134,20 @@ func NewTestNDESAdminServer(t *testing.T, responseTemplate string, responseStatu
|
||||
return ndesAdminServer
|
||||
}
|
||||
|
||||
func NewTestDynamicChallengeServer(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
dynamicChallengeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Println(r.URL.Path)
|
||||
_, err := w.Write([]byte("dynamic challenge"))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
t.Cleanup(dynamicChallengeServer.Close)
|
||||
|
||||
return dynamicChallengeServer
|
||||
}
|
||||
|
||||
// utf16FromString returns the UTF-16 encoding of the UTF-8 string s, with a terminating NUL added.
|
||||
// If s contains a NUL byte at any location, it returns (nil, syscall.EINVAL).
|
||||
func utf16FromString(s string) ([]uint16, error) {
|
||||
|
||||
Reference in New Issue
Block a user