From bb4a68a0e9364045faf4946f0b7b17a409323e5d Mon Sep 17 00:00:00 2001 From: Phil Porada Date: Thu, 15 Aug 2024 12:15:42 -0700 Subject: [PATCH] Working, but without tests --- cmd/admin/pause.go | 140 +++++++++++++++--- cmd/admin/testdata/test_pause_data_empty.csv | 0 .../test_pause_data_invalid_noDomain.csv | 1 + ...st_pause_data_invalid_noIdentifierType.csv | 1 + cmd/admin/testdata/test_pause_data_valid.csv | 6 + test/certs/generate.sh | 2 +- test/config-next/ra.json | 1 + test/config/ra.json | 1 + 8 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 cmd/admin/testdata/test_pause_data_empty.csv create mode 100644 cmd/admin/testdata/test_pause_data_invalid_noDomain.csv create mode 100644 cmd/admin/testdata/test_pause_data_invalid_noIdentifierType.csv create mode 100644 cmd/admin/testdata/test_pause_data_valid.csv diff --git a/cmd/admin/pause.go b/cmd/admin/pause.go index f0f629874c5..186793f1386 100644 --- a/cmd/admin/pause.go +++ b/cmd/admin/pause.go @@ -2,15 +2,21 @@ package main import ( "context" + "crypto/sha256" + "encoding/base64" "encoding/csv" "errors" "flag" "fmt" "io" "os" + "slices" "strconv" + "strings" "github.com/letsencrypt/boulder/identifier" + rapb "github.com/letsencrypt/boulder/ra/proto" + sapb "github.com/letsencrypt/boulder/sa/proto" ) // subcommandPauseBatch encapsulates the "admin pause-batch" commands. @@ -33,13 +39,17 @@ func (p *subcommandPauseBatch) Run(ctx context.Context, a *admin) error { return errors.New("the -file flag is required") } - _, err := a.readPausedAccountFile(p.file) + identifiers, err := a.readPausedAccountFile(p.file) if err != nil { return err } - // TODO: Fix - return errors.New("no action to perform on the given CSV file was specified") + err = a.pauseIdentifiers(identifiers) + if err != nil { + return err + } + + return nil } // subcommandUnpauseBatch encapsulates the "admin unpause-batch" commands. @@ -62,13 +72,17 @@ func (u *subcommandUnpauseBatch) Run(ctx context.Context, a *admin) error { return errors.New("the -file flag is required") } - _, err := a.readPausedAccountFile(u.file) + identifiers, err := a.readPausedAccountFile(u.file) + if err != nil { + return err + } + + err = a.unpauseAccount(identifiers) if err != nil { return err } - // TODO: Fix - return errors.New("no action to perform on the given CSV file was specified") + return nil } // pauseData contains @@ -78,6 +92,51 @@ type pauseData struct { identifierValue []string } +// pauseIdentifiers allows administratively pausing a set of domain names for an +// account. +func (a *admin) pauseIdentifiers(pd []pauseData) error { + if len(pd) <= 0 { + return errors.New("cannot pause identifiers because no pauseData was sent") + } + + for _, data := range pd { + req := sapb.PauseRequest{ + RegistrationID: data.accountID, + Identifiers: []*sapb.Identifier{{ + Type: string(data.identifierType), + Value: strings.Join(data.identifierValue, ","), + }, + }, + } + _, err := a.sac.PauseIdentifiers(context.Background(), &req) + if err != nil { + return err + } + } + + return nil +} + +// unpauseAccount allows administratively unpausing all identifiers for an +// account. +func (a *admin) unpauseAccount(pd []pauseData) error { + if len(pd) <= 0 { + return errors.New("cannot unpause accounts because no pauseData was sent") + } + + for _, data := range pd { + req := rapb.UnpauseAccountRequest{ + RegistrationID: data.accountID, + } + _, err := a.rac.UnpauseAccount(context.Background(), &req) + if err != nil { + return err + } + } + + return nil +} + // readPausedAccountFile parses the contents of a CSV into a slice of // `pauseData` objects. It will return an error if an individual record is // malformed. @@ -94,32 +153,79 @@ func (a *admin) readPausedAccountFile(filePath string) ([]pauseData, error) { reader.FieldsPerRecord = -1 reader.TrimLeadingSpace = true - var data []pauseData + var parsedRecords []pauseData + hashToPauseData := make(map[string]pauseData) + lineCounter := 1 + + defer func() { + var record string + if len(hashToPauseData) == 1 { + record = "record" + } else { + record = "records" + } + fmt.Fprintf(os.Stderr, "detected %d valid %s from input file\n", len(hashToPauseData), record) + }() - // Parse file contents + // Process contents of the CSV file for { record, err := reader.Read() if errors.Is(err, io.EOF) { // Finished parsing the file. - if len(record) == 0 { - return nil, errors.New("no records found") + //if len(record) == 0 { + // return nil, errors.New("no records found") + //} + + for _, value := range hashToPauseData { + parsedRecords = append(parsedRecords, value) } - // TODO: return valid data or something - return data, nil + + return parsedRecords, nil } else if err != nil { return nil, err } - // Ensure the first column of each record can be parsed as a valid - // accountID. recordID := record[0] accountID, err := strconv.ParseInt(recordID, 10, 64) if err != nil { - return nil, fmt.Errorf("%q couldn't be parsed as an accountID due to: %s", recordID, err) + fmt.Fprintf(os.Stderr, "skipping: malformed accountID entry on line %d\n", lineCounter) + continue + } + + // Ensure that an identifier type is present, otherwise skip the line. + if len(record[1]) == 0 { + fmt.Fprintf(os.Stderr, "skipping: malformed identifierType entry on line %d\n", lineCounter) + continue } identifierType := identifier.IdentifierType(record[1]) - identifierValue := record[2:] - fmt.Printf("Loaded: %d,%s,%s", accountID, identifierType, identifierValue[:]) + if len(record) < 3 { + fmt.Fprintf(os.Stderr, "skipping: malformed identifierValue entry on line %d\n", lineCounter) + continue + } + // The remaining fields are the domain names. + identifierValue := record[2:] + slices.Sort(identifierValue) + + // Construct a hash over the parsed line from the CSV. The hash will be + // used as a key mapping to a pauseData object containing the fields we + // wish to operate on. + hash := sha256.New() + var recordBytes []byte + recordBytes = append(recordBytes, byte(accountID)) + recordBytes = append(recordBytes, []byte(identifierType)...) + recordBytes = append(recordBytes, []byte(strings.Join(identifierValue, ""))...) + b64Hash := base64.StdEncoding.EncodeToString(hash.Sum(recordBytes)) + + if _, ok := hashToPauseData[b64Hash]; !ok { + hashToPauseData[b64Hash] = pauseData{ + accountID: accountID, + identifierType: identifierType, + identifierValue: identifierValue, + } + } else { + fmt.Fprintf(os.Stderr, "skipping: duplicate entry on line %d: %d,%s,%s\n", lineCounter, accountID, identifierType, identifierValue[:]) + } + lineCounter++ } } diff --git a/cmd/admin/testdata/test_pause_data_empty.csv b/cmd/admin/testdata/test_pause_data_empty.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cmd/admin/testdata/test_pause_data_invalid_noDomain.csv b/cmd/admin/testdata/test_pause_data_invalid_noDomain.csv new file mode 100644 index 00000000000..444357a4f98 --- /dev/null +++ b/cmd/admin/testdata/test_pause_data_invalid_noDomain.csv @@ -0,0 +1 @@ +1,dns \ No newline at end of file diff --git a/cmd/admin/testdata/test_pause_data_invalid_noIdentifierType.csv b/cmd/admin/testdata/test_pause_data_invalid_noIdentifierType.csv new file mode 100644 index 00000000000..56a6051ca2b --- /dev/null +++ b/cmd/admin/testdata/test_pause_data_invalid_noIdentifierType.csv @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/cmd/admin/testdata/test_pause_data_valid.csv b/cmd/admin/testdata/test_pause_data_valid.csv new file mode 100644 index 00000000000..71040007c59 --- /dev/null +++ b/cmd/admin/testdata/test_pause_data_valid.csv @@ -0,0 +1,6 @@ +1,dns,example.com,example.net +2,dns,example.org +1,dns,example.com,example.net +1,dns,example.com,example.net +3,dns,example.gov +3,dns,example.gov \ No newline at end of file diff --git a/test/certs/generate.sh b/test/certs/generate.sh index 9e1a6c3036d..827cc20ea9b 100755 --- a/test/certs/generate.sh +++ b/test/certs/generate.sh @@ -40,7 +40,7 @@ ipki() ( minica -domains redis -ip-addresses 10.33.33.2,10.33.33.3,10.33.33.4,10.33.33.5,10.33.33.6,10.33.33.7,10.33.33.8,10.33.33.9 # Used by Boulder gRPC services as both server and client mTLS certificates. - for SERVICE in admin-revoker expiration-mailer ocsp-responder consul \ + for SERVICE in admin admin-revoker expiration-mailer ocsp-responder consul \ wfe akamai-purger bad-key-revoker crl-updater crl-storer \ health-checker rocsp-tool sfe; do minica -domains "${SERVICE}.boulder" & diff --git a/test/config-next/ra.json b/test/config-next/ra.json index ba51e906a23..e99ef1961db 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -91,6 +91,7 @@ "ra.RegistrationAuthority": { "clientNames": [ "admin-revoker.boulder", + "admin.boulder", "bad-key-revoker.boulder", "ocsp-responder.boulder", "wfe.boulder", diff --git a/test/config/ra.json b/test/config/ra.json index a9e0afb9560..c5f2e00570f 100644 --- a/test/config/ra.json +++ b/test/config/ra.json @@ -93,6 +93,7 @@ "ra.RegistrationAuthority": { "clientNames": [ "admin-revoker.boulder", + "admin.boulder", "bad-key-revoker.boulder", "ocsp-responder.boulder", "sfe.boulder",