diff --git a/doc/README.md b/doc/README.md index 4da7773..f0a109f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -5,6 +5,7 @@ The following topics are documented: * [Telemetry Types](telemetrytype.md) * [Telemetry Tags](telemetrytag.md) * [Telemetry Timestamps](telemetrytimestamp.md) -* [Telemetry Client Ids](telemetryclientid.md) +* [Telemetry Client Registration](telemetryclientregistration.md) +* [Telemetry Registration Ids](telemetryregistrationid.md) * [Telemetry Relay](telemetryrelay.md) * [Telemetry Client REST API](api/) \ No newline at end of file diff --git a/doc/api/README.md b/doc/api/README.md index fcdaaf8..f60d3d1 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -15,20 +15,20 @@ A telemetry client is expected to support the following workflow: # Registration For a telemetry client to be able to register with an upstream telemetry -server, it will need to generate a clientInstanceId value, which is used -to uniquely identify a given client with the upstream server, and should +server, it will need to generate a registration value, which is used to +uniquely identify a given client system with the upstream server, and should store this value in a secure fashion so that it can be accessed later when (re-)authenticating with the upstream telemetry server. When a telemetry client registers with the upstream telemetry server, using the [/register](requests/register.md) request, it will send a request payload -containing the clientInstanceId, and the successful response will provide +containing the registration, and the successful response will provide a set of client credentials as follows: | Name | Type | Description | | ---- | ---- | ----------- | -| clientId | integer($int64) | ID used to identify the client to the server | -| authToken | string($([JWT](https://jwt.io/)) | A JSON Web Token ([JWT](https://jwt.io/)) authorization token | +| registrationId | integer($int64) | ID used to identify the client system to the server | +| authToken | string($([jwt](https://jwt.io/)) | A JSON Web Token ([JWT](https://jwt.io/)) authorization token | | registrationDate | string($[rfc3339nano](https://pkg.go.dev/time#pkg-constants)) | The client UTC registration timestamp expressed in
[RFC3339nano](https://pkg.go.dev/time#pkg-constants) format | ***NOTE***: The telemetry client is responsible for storing these client @@ -41,7 +41,7 @@ server it must prove that it has the authorization to do so. This is achieved by supplying the appropriate request headers: * [Authorization](headers/authorization.md) -* [X-Telemetry-Client-Id](headers/telemetry-client-id.md) +* [X-Telemetry-Registration-Id](headers/telemetry-registration-id.md) When a telemetry client submits a telemetry report to the upstream telemetry server, using the [/report](requests/report.md) @@ -54,11 +54,11 @@ objects. # (Re-)Authentication For a telemetry client to (re-)authenticate with an upstream telemetry server, it will need to generate a supported hash, e.g. `sha256`, of the -clientInstanceId to validate that it is in fact that client in question. +registration to validate that it is in fact that client in question. When a telemetry client (re-)authenticates with the upstream telemetry server, using the [/authenticate](requests/authenticate.md) request, it will -send a request payload containing it's clientId and an instIdHash, +send a request payload containing it's registratiionId and an registrationHash, specifying the hash method and associated value, and the successful response will provide a set of client credentials, the same as for a [/register](requests/register.md) request, as described [above][#registration]. \ No newline at end of file diff --git a/doc/api/headers/telemetry-client-id.md b/doc/api/headers/telemetry-client-id.md deleted file mode 100644 index 387da5d..0000000 --- a/doc/api/headers/telemetry-client-id.md +++ /dev/null @@ -1,9 +0,0 @@ -# Telemetry Client Id Header -When a telemetry client is submitting a telemetry report using -the [/report](../requests/report.md) request it will need to provide a -`X-Telemetry-Client-Id` header specifying the clientId from the client -credentials obtained using the [/register](../requests/register.md) request. - -## Format of the Telemetry Client Id Header -The `X-Telemetry-Client-Id` header value should be the string -representation of the int64 clientId value. \ No newline at end of file diff --git a/doc/api/headers/telemetry-registration-id.md b/doc/api/headers/telemetry-registration-id.md new file mode 100644 index 0000000..c5fb598 --- /dev/null +++ b/doc/api/headers/telemetry-registration-id.md @@ -0,0 +1,10 @@ +# Telemetry Registration Id Header +When a telemetry client is submitting a telemetry report using the +[/report](../requests/report.md) request it will need to provide a +`X-Telemetry-Registration-Id` header specifying the registrationId +from the client credentials obtained using the +[/register](../requests/register.md) request. + +## Format of the Telemetry Registration Id Header +The `X-Telemetry-Registration-Id` header value should be the string +representation of the int64 registrationId value. \ No newline at end of file diff --git a/doc/api/requests/authenticate.md b/doc/api/requests/authenticate.md index dd04af8..a913c3a 100644 --- a/doc/api/requests/authenticate.md +++ b/doc/api/requests/authenticate.md @@ -6,7 +6,7 @@ Type: **POST** | Name | Type | Description | Example | | ---- | ---- | ----------- | ------- | -| body | object | {
  clientId integer($int64)
  instIdHash {
    method string
    value string
  }
} | {
  "clientId": 1234567890
  "instIdHash": {
    "method": "sha256"
    "value": "984271ec70628b47995fdf9271ded6274c2b104ce201164a9b63cfefef7f40d0"
  }
}| +| body | object | {
  registrationId integer($int64)
  regHash {
    method string
    value string
  }
} | {
  "registrationId": 1234567890
  "regHash": {
    "method": "sha256"
    "value": "984271ec70628b47995fdf9271ded6274c2b104ce201164a9b63cfefef7f40d0"
  }
}| Request body type `ClientAuthenticationRequest` defined in [restapi module](../../../pkg/restapi) @@ -14,9 +14,9 @@ Request body type `ClientAuthenticationRequest` defined in [restapi module](../. | Code | Description | Example | | ---- | ----------- | ------- | -| 200 | Success
`Content-Type: application/json`
{
  clientId integer($int64)
  authToken string
  registrationDate string
} | {
  "clientId": 1234567890
  "authToken": "encoded.JWT.token"
  "registrationDate": "2024-08-01T01:02:03.000000Z"
} | -| 400 | Bad Request
Missing or incompatible body
`Content-Type: application/json`
{
  error string
} | {
  "error": "no clientInstanceId value provided"
} | -| 401 | Unauthorized
Client (re-)registration required due to one of:
- specified client is not registered
- invalid clientId provided
- provided clientInstanceId hash doesn't match
[WWW-Authenticate](../headers/www-authenticate.md) response header will specify recovery action
`Content-Type: application/json`
{
  error string
} | {
  "error": "Client not registered"
} | +| 200 | Success
`Content-Type: application/json`
{
  registrationId integer($int64)
  authToken string
  registrationDate string
} | {
  "registrationId": 1234567890
  "authToken": "encoded.JWT.token"
  "registrationDate": "2024-08-01T01:02:03.000000Z"
} | +| 400 | Bad Request
Missing or incompatible body
`Content-Type: application/json`
{
  error string
} | {
  "error": "no registration hash value provided"
} | +| 401 | Unauthorized
Client (re-)registration required due to one of:
- specified client is not registered
- invalid registrationId provided
- provided registration hash doesn't match
[WWW-Authenticate](../headers/www-authenticate.md) response header will specify recovery action
`Content-Type: application/json`
{
  error string
} | {
  "error": "Client not registered"
} | Response success body type `ClientAuthenticationResponse` defined in [restapi module](../../../pkg/restapi) diff --git a/doc/api/requests/register.md b/doc/api/requests/register.md index 1897707..23c6eb0 100644 --- a/doc/api/requests/register.md +++ b/doc/api/requests/register.md @@ -6,7 +6,7 @@ Type: **POST** | Name | Type | Description | Example | | ---- | ---- | ----------- | ------- | -| body | object | {
  clientInstanceId: string
} | {
  "clientInstanceId": "ba2cb9f4927441602a385b27f502134902b636f395cadb3ea1438084dba29c8c"
}| +| body | object | {
  clientRegistration: {
    clientId: string
    systemUUID: string
    timestamp: string($[rfc3339nano](https://pkg.go.dev/time#pkg-constants))
  }
} | {
  "clientRegistration": {
    "clientId": "f323628e-c1cc-45d4-824d-22d4d6f0fd01"
    "systemUUID": "74f0f0b0-fb29-4405-a0b8-4e7747bdfd8a"
    "timestamp": "2024-08-01T00:01:02.000000Z"
  }
}| Request body type `ClientRegistrationRequest` defined in [restapi module](../../../pkg/restapi/) @@ -14,8 +14,8 @@ Request body type `ClientRegistrationRequest` defined in [restapi module](../../ | Code | Description | Example | | ---- | ----------- | ------- | -| 200 | Success
`Content-Type: application/json`
{
  clientId integer($int64)
  authToken string
  registrationDate string
} | {
  "clientId": 1234567890
  "authToken": "encoded.JWT.token"
  "registrationDate": "2024-08-01T01:02:03.000000Z"
} | -| 400 | Bad Request
Missing or incompatible body
`Content-Type: application/json`
{
  error string
} | {
  "error": "no clientInstanceId value provided"
} | -| 409 | Conflict
Client Instance Id already registered
`Content-Type: application/json`
{
  error string
} | {
  "error": "specified clientInstanceId already exists"
} | +| 200 | Success
`Content-Type: application/json`
{
  registrationId integer($int64)
  authToken string
  registrationDate string
} | {
  "registrationId": 1234567890
  "authToken": "encoded.JWT.token"
  "registrationDate": "2024-08-01T01:02:03.000000Z"
} | +| 400 | Bad Request
Missing or incompatible body
`Content-Type: application/json`
{
  error string
} | {
  "error": "missing registration clientId"
}
or
{
  "error": "missing registration timestamp"
} | +| 409 | Conflict
Client Registration or Registration's Client Id already registered
`Content-Type: application/json`
{
  error string
} | {
  "error": "specified registration already exists"
}
or
{
  "error": "specified registration clientId already exists"
} | Response success body type `ClientRegistrationResponse` defined in [restapi module](../../../pkg/restapi/) \ No newline at end of file diff --git a/doc/api/structs/telemetrybundle.md b/doc/api/structs/telemetrybundle.md index 20afa6a..c8884be 100644 --- a/doc/api/structs/telemetrybundle.md +++ b/doc/api/structs/telemetrybundle.md @@ -8,8 +8,8 @@ The TelemetryBundle data structure consists of the following sections: * bundleTimeStamp - the UTC timestamp for when the bundle was generated, formatted in [RFC3339nano](../../telemetrytimestamp.md) format * bundleClientId - the clientId of the telemetry client generating this - bundle, as specified in the credentials returned when the telemetry - client successfully registered with the upstream telemetry server. + bundle, as specified in the registraion used to successfully register + the client with the upstream telemetry server. * bundleCustomerId - a string value specify the customer identifier, if any, associated with the telemetry client * bundleAnnotations - a possibly empty list of @@ -25,7 +25,7 @@ in a bundle must originate from the same telemetry client. header { bundleId string bundleTimeStamp string($rfc3339nano) - bundleClientId integer($int64) + bundleClientId string bundleCustomerId string bundleAnnotations [ string... diff --git a/doc/api/structs/telemetryreport.md b/doc/api/structs/telemetryreport.md index cbb71fc..3fa708d 100644 --- a/doc/api/structs/telemetryreport.md +++ b/doc/api/structs/telemetryreport.md @@ -8,8 +8,8 @@ The TelemetryReport data structure consists of the following sections: * reportTimeStamp - the UTC timestamp for when the bundle was generated, formatted in [RFC3339nano](../../telemetrytimestamp.md) format * reportClientId - the clientId of the telemetry client generating this - report, as specified in the credentials returned when the telemetry - client successfully registered with the upstream telemetry server. + report, as specified in the registraion used to successfully register + the client with the upstream telemetry server. * reportAnnotations - a possibly empty list of [telemetry annotation tags](../../telemetrytag.md) * payload - a list of one or more [TelemetryBundle](telemetrybundle.md) objects @@ -24,7 +24,7 @@ relayed through one or more telemetry relays. header { reportId string reportTimeStamp string($rfc3339nano) - reportClientId integer($int64) + reportClientId string reportAnnotations [ string... ] diff --git a/doc/telemetryclientid.md b/doc/telemetryclientid.md deleted file mode 100644 index d274046..0000000 --- a/doc/telemetryclientid.md +++ /dev/null @@ -1,12 +0,0 @@ -# Telemetry Client Ids - -A telemetry server (or relay) manages a pool of telemetry client ids, -current ranging from 1 to MAX_INT64. - -When a client [registers](api/requests/register.md) with an upstream -telemetry server (or relay) it is assigned a new client id. - -This telemetry client id only has meaning in the context of a specific -combination of telemetry client and telemetry server, and the same -client id can be assigned to multiple telemetry clients so long as they -are talking to different upstream telemery servers (or relays). \ No newline at end of file diff --git a/doc/telemetryclientregistration.md b/doc/telemetryclientregistration.md new file mode 100644 index 0000000..2bc902d --- /dev/null +++ b/doc/telemetryclientregistration.md @@ -0,0 +1,45 @@ +# Telemetry Client Registration + +To submit telemetry reports to an upstream telemetry service (gateway +or relay) a client must [register](api/requests/register.md) with that +service. As part of the registration process a client will generate a +client registration structure which is used to uniquely identify it as +a telemetry service client. + +## Client Registration Structure +This client registration structure has the following format: +``` +{ + "clientId": "", + "systemUUID": "", + "timestamp": "" +} +``` + +## Client Registration Collisions +A client system may be required to generate a new client registration +if the upstream server detects that the registration is a duplicate +of an existing registration, or that the registration's clientId is +a duplicate of existing client's clientId. + +The client registration's clientId value is used to identify the +client generating a [telemetry bundle](api/structs/telemetrybundle.md) +and submitting a [telemetry report](api/structs/telemetryreport.md). + +## Uniquely Identifying a client +On its own the clientId may not always uniquely identify a client +within the overall pool of telemetry service submissions, because two +client systems could independently generate the same UUID value to use +as a clientId, but a telemetry client's clientId will always be unique +with respect to other clients of the same upstream telemetry service. + +This property, that clientIds will always be unique with respect to +other clients of the same upstream telemetry service, can be leveraged +by [telemetry relays](telemetryrelay.md) to assist in uniquely +identifying telemetry clients. When telemetry is relayed, the relay +will add a RELAYED_VIA tag to the telemetry submission which identifies +both the relay and the client that submitted the telemetry to the +relay. The aggregate of the RELAYED_VIA tag values associated with a +telemetry data item can thus be used to uniquely identify the path +from the originating client to the main telemetry service gateway, +and thus uniquely identify a specific client's telemetry submissions. diff --git a/doc/telemetryregistrationid.md b/doc/telemetryregistrationid.md new file mode 100644 index 0000000..36e1317 --- /dev/null +++ b/doc/telemetryregistrationid.md @@ -0,0 +1,17 @@ +# Telemetry Registration Ids + +A telemetry server (or relay) manages a pool of telemetry client +registration ids, currently ranging from 1 to MAX_INT64. + +Once a client has successfully [registered](api/requests/register.md) +it's [client registration](telemetryclientregistration.md) with an +upstream telemetry service the response will contain the client's +credentials, including the registrationId which will be used to set +the [X-Telemetry-Registration-Id](api/headers/telemetry-registration-id.md) +when submitting telemetry requests to the upstream telemetry service. + +Note that this telemetry registration id only has meaning in the +context of a specific combination of telemetry client and telemetry +server, and the same registration id may be assigned to multiple +telemetry clients so long as they are talking to different upstream +telemery servers (or relays). \ No newline at end of file diff --git a/doc/telemetryrelay.md b/doc/telemetryrelay.md index 5c5e4ff..11c2d38 100644 --- a/doc/telemetryrelay.md +++ b/doc/telemetryrelay.md @@ -15,8 +15,9 @@ will be processed as follows: the [telemetry bundles](api/structs/telemetrybundle.md). 2. The telemetry relay will annotate each bundle with a new [RELAYED_VIA tag](telemetrytag.md) whose value consists of the - [client Id](telemetryclientid.md) of the client that submitted the report, and the telemetry - relay's own client Id, joined with a `:`. + [registration Id](telemetryregistrationid.md) of the client that + submitted the report, and the telemetry relay's own registration Id, + joined with a `:`. 3. The telemetry relay will stage the received telemetry bundles locally. 4. Once sufficient bundles are available, or enough time has passed, diff --git a/doc/telemetrytag.md b/doc/telemetrytag.md index 6a4e804..b6c09e8 100644 --- a/doc/telemetrytag.md +++ b/doc/telemetrytag.md @@ -29,12 +29,12 @@ inheritance rules: ## Telemetry Annotation Tag Examples Some examples: -* when bundles are relayed via a telemetry relay server a RELAYED_VIA - tag composed of the combination of the client id of the reporting - client and the relay server's client id will be added to the relayed - bundles. -* telemetry relayed via a proxy service may be annotated by the proxy - type, e.g +* when bundles are relayed via a [telemetry relay](telemetryrelay.md) + server a RELAYED_VIA tag composed of the combination of the registration + id of the reporting client and the relay server's registration id will + be added to the relayed bundles. +* telemetry relayed or synthesised via a proxy service could be annotated + by the proxy type, e.g * PROXY_TYPE=RMT for RMT - * PROXY_TYPE=SUMA for SUSE Manager + * PROXY_TYPE=MLM for Multi Linux Manager * PROXY_TYPE=SCC for SCC \ No newline at end of file diff --git a/pkg/client/authenticate.go b/pkg/client/authenticate.go index 47a2d88..3b73565 100644 --- a/pkg/client/authenticate.go +++ b/pkg/client/authenticate.go @@ -15,6 +15,12 @@ import ( // Authenticate is responsible for (re)authenticating an already registered // client with the server to ensure that it's auth token is up to date. func (tc *TelemetryClient) Authenticate() (err error) { + // get the registration, failing if it can't be retrieved + regId, err := tc.getRegistration() + if err != nil { + return + } + if err = tc.loadTelemetryAuth(); err != nil { return fmt.Errorf( "telemetry client (re-)authentication requires an existing "+ @@ -23,16 +29,10 @@ func (tc *TelemetryClient) Authenticate() (err error) { ) } - // get the instanceId, failing if it can't be retrieved - instId, err := tc.getInstanceId() - if err != nil { - return - } - // assemble the authentication request caReq := restapi.ClientAuthenticationRequest{ - ClientId: tc.auth.ClientId, - InstIdHash: *instId.Hash("default"), + RegistrationId: tc.auth.RegistrationId, + RegHash: *regId.Hash("default"), } reqBodyJSON, err := json.Marshal(&caReq) @@ -92,7 +92,7 @@ func (tc *TelemetryClient) Authenticate() (err error) { return } - tc.auth.ClientId = caResp.ClientId + tc.auth.RegistrationId = caResp.RegistrationId tc.auth.Token = types.TelemetryAuthToken(caResp.AuthToken) tc.auth.RegistrationDate, err = types.TimeStampFromString(caResp.RegistrationDate) if err != nil { @@ -115,7 +115,7 @@ func (tc *TelemetryClient) Authenticate() (err error) { slog.Debug( "successfully authenticated", - slog.Int64("clientId", tc.auth.ClientId), + slog.Int64("registrationId", tc.auth.RegistrationId), ) return diff --git a/pkg/client/client.go b/pkg/client/client.go index 5f20017..7112135 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,7 +1,6 @@ package client import ( - "encoding/base64" "encoding/json" "errors" "fmt" @@ -20,20 +19,22 @@ import ( const ( //CONFIG_DIR = "/etc/susetelemetry" - CONFIG_DIR = "/tmp/susetelemetry" - CONFIG_PATH = CONFIG_DIR + "/config.yaml" - AUTH_PATH = CONFIG_DIR + "/auth.json" - INSTANCEID_PATH = CONFIG_DIR + "/instanceid" + CONFIG_DIR = "/tmp/susetelemetry" + CONFIG_PATH = CONFIG_DIR + "/config.yaml" + AUTH_PATH = CONFIG_DIR + "/auth.json" ) +// TODO: unify with restapi.ClientRegistrationResponse type TelemetryAuth struct { - ClientId int64 `json:"clientId"` - Token types.TelemetryAuthToken `json:"token"` + // TODO: unify these fields with restapi.ClientRegistrationResponse + RegistrationId int64 `json:"registrationId"` + Token types.TelemetryAuthToken `json:"authToken"` RegistrationDate types.TelemetryTimeStamp `json:"registrationDate"` } type TelemetryClient struct { cfg *config.Config + reg *TelemetryClientRegistration auth TelemetryAuth authLoaded bool processor telemetrylib.TelemetryProcessor @@ -41,6 +42,7 @@ type TelemetryClient struct { func NewTelemetryClient(cfg *config.Config) (tc *TelemetryClient, err error) { tc = &TelemetryClient{cfg: cfg} + tc.reg = NewTelemetryClientRegistration() tc.processor, err = telemetrylib.NewTelemetryProcessor(&cfg.DataStores) return } @@ -69,30 +71,85 @@ func checkFileReadAccessible(filePath string) bool { return true } -func ensureInstanceIdExists(instIdPath string) error { +func (tc *TelemetryClient) getRegistration() (reg types.ClientRegistration, err error) { + // ensure that a registration exists, creating one if needed + err = tc.ensureRegistrationExists() + if err != nil { + return + } + + // if a registration is already loaded then nothing more to do + if tc.reg.Valid() { + reg = tc.reg.Registration() + return + } + + err = tc.reg.Load() + if err != nil { + slog.Debug( + "failed to load client registration", + slog.String("reg", tc.reg.Path()), + slog.String("err", err.Error()), + ) + return + } + + slog.Debug( + "successfully loaded client registration", + slog.String("reg", tc.reg.String()), + ) - slog.Debug("ensuring existence of instIdPath", slog.String("instIdPath", instIdPath)) - _, err := os.Stat(instIdPath) - if !os.IsNotExist(err) { + reg = tc.reg.ClientRegistration + + return +} + +func (tc *TelemetryClient) ensureRegistrationExists() (err error) { + // if the existing client registration is valid then nothing to do + if tc.reg.Valid() { + slog.Debug( + "client registration already loaded", + slog.String("reg", tc.reg.String()), + ) + return nil + } + + slog.Debug( + "ensuring existence of client registration", + slog.String("regPath", tc.reg.path), + ) + + if tc.reg.Accessible() { + slog.Debug( + "client registration exists, needs to be loaded", + slog.String("regPath", tc.reg.Path()), + ) return nil } - // For now generate an instanceId as a base64 encoded timestamp - now := types.Now().String() - instId := make([]byte, base64.StdEncoding.EncodedLen(len(now))) - base64.StdEncoding.Encode(instId, []byte(now)) + // need to generate a new client registration if needed + tc.reg.Generate() + slog.Debug( + "client registration generated", + slog.String("reg", tc.reg.String()), + ) - err = os.WriteFile(instIdPath, instId, 0600) + // save the generated client registration + err = tc.reg.Save() if err != nil { - slog.Error( - "failed to write instId to instIdPath", - slog.String("instId", string(instId)), - slog.String("instIdPath", instIdPath), - slog.String("err", err.Error()), + slog.Debug( + "failed to save generated client registration", + slog.String("reg", tc.reg.String()), ) + return err } - return nil + slog.Debug( + "saved client registration", + slog.String("reg", tc.reg.String()), + ) + + return } func (tc *TelemetryClient) authParsedToken() (token *jwt.Token, err error) { @@ -164,18 +221,10 @@ func (tc *TelemetryClient) AuthAccessible() bool { return checkFileReadAccessible(tc.AuthPath()) } -func (tc *TelemetryClient) InstanceIdAccessible() bool { - return checkFileReadAccessible(tc.InstIdPath()) -} - func (tc *TelemetryClient) HasAuth() bool { return checkFileExists(tc.AuthPath()) } -func (tc *TelemetryClient) HasInstanceId() bool { - return checkFileExists(tc.InstIdPath()) -} - func (tc *TelemetryClient) Processor() telemetrylib.TelemetryProcessor { // may want to just make the processor a public field return tc.processor @@ -186,38 +235,16 @@ func (tc *TelemetryClient) AuthPath() string { return AUTH_PATH } -func (tc *TelemetryClient) InstIdPath() string { - // hard coded for now, possibly make a config option - return INSTANCEID_PATH +func (tc *TelemetryClient) ClientId() string { + return tc.reg.ClientId } -func (tc *TelemetryClient) getInstanceId() (instId types.ClientInstanceId, err error) { - instIdPath := tc.InstIdPath() - - err = ensureInstanceIdExists(instIdPath) - if err != nil { - return - } - - data, err := os.ReadFile(instIdPath) - if err != nil { - slog.Error( - "failed to read instId file", - slog.String("path", instIdPath), - slog.String("err", err.Error()), - ) - return - } - - instId = types.ClientInstanceId((data)) - - slog.Debug( - "successfully read instId file", - slog.String("path", string(instIdPath)), - slog.String("instId", string(instId)), - ) +func (tc *TelemetryClient) RegistrationPath() string { + return tc.reg.path +} - return +func (tc *TelemetryClient) RegistrationAccessible() bool { + return tc.reg.Accessible() } func (tc *TelemetryClient) deleteTelemetryAuth() (err error) { @@ -271,8 +298,8 @@ func (tc *TelemetryClient) loadTelemetryAuth() (err error) { return } - if tc.auth.ClientId <= 0 { - err = fmt.Errorf("invalid client id") + if tc.auth.RegistrationId <= 0 { + err = fmt.Errorf("invalid registration id") slog.Error( "invalid auth", slog.String("authPath", authPath), @@ -494,7 +521,7 @@ func (tc *TelemetryClient) Generate(telemetry types.TelemetryType, content *type func (tc *TelemetryClient) CreateBundles(tags types.Tags) error { // Bundle existing telemetry data items found in DataItem data store into one or more bundles in the Bundle data store slog.Debug("Bundle", slog.String("Tags", tags.String())) - tc.processor.GenerateBundle(tc.auth.ClientId, tc.cfg.CustomerID, tags) + tc.processor.GenerateBundle(tc.ClientId(), tc.cfg.CustomerID, tags) return nil } @@ -502,7 +529,7 @@ func (tc *TelemetryClient) CreateBundles(tags types.Tags) error { func (tc *TelemetryClient) CreateReports(tags types.Tags) (err error) { // Generate reports from available bundles slog.Debug("CreateReports", slog.String("Tags", tags.String())) - tc.processor.GenerateReport(tc.auth.ClientId, tags) + tc.processor.GenerateReport(tc.ClientId(), tags) return } diff --git a/pkg/client/register.go b/pkg/client/register.go index 2f20087..fe3c220 100644 --- a/pkg/client/register.go +++ b/pkg/client/register.go @@ -13,22 +13,22 @@ import ( ) func (tc *TelemetryClient) Register() (err error) { - // get the saved TelemetryAuth, returning success if found - err = tc.loadTelemetryAuth() - if err == nil { - slog.Debug("telemetry auth found, client already registered, skipping", slog.Int64("clientId", tc.auth.ClientId)) + // get the registration, failing if it can't be retrieved + reg, err := tc.getRegistration() + if err != nil { return } - // get the instanceId, failing if it can't be retrieved - instId, err := tc.getInstanceId() - if err != nil { + // get the saved TelemetryAuth, returning success if found + err = tc.loadTelemetryAuth() + if err == nil { + slog.Debug("telemetry auth found, client already registered, skipping", slog.Int64("registrationId", tc.auth.RegistrationId)) return } // register the system as a client var crReq restapi.ClientRegistrationRequest - crReq.ClientInstanceId = instId + crReq.ClientRegistration = reg reqBodyJSON, err := json.Marshal(&crReq) if err != nil { slog.Error( @@ -71,8 +71,37 @@ func (tc *TelemetryClient) Register() (err error) { return } - // TODO: Handle http.StatusConflict (409) as needing to regenerate instId - if resp.StatusCode != http.StatusOK { + // check the response status code, and handle appropriately + switch resp.StatusCode { + case http.StatusOK: + // all good, nothing to do + + case http.StatusConflict: + slog.Debug( + "StatusConflict returned", + slog.Int("StatusCode", resp.StatusCode), + slog.String("error", string(respBody)), + ) + // retry if a duplicate client registration attempt is detected + if tc.reg.RetriesEnabled() { + slog.Warn( + "Duplicate client registration detected, forcing re-registration", + ) + + // delete the existing registration, forcing it to be regenerated as + // part of the next client registration attempt + tc.reg.Remove() + + // disable further retries + tc.reg.DisableRetries() + + // retry client registration + return tc.Register() + } + fallthrough + + default: + // unhandled error so fail appropriately err = fmt.Errorf("client registration failed: %s", string(respBody)) return } @@ -87,7 +116,7 @@ func (tc *TelemetryClient) Register() (err error) { return } - tc.auth.ClientId = crResp.ClientId + tc.auth.RegistrationId = crResp.RegistrationId tc.auth.Token = types.TelemetryAuthToken(crResp.AuthToken) tc.auth.RegistrationDate, err = types.TimeStampFromString(crResp.RegistrationDate) if err != nil { @@ -112,7 +141,7 @@ func (tc *TelemetryClient) Register() (err error) { slog.Debug( "successfully registered as client", - slog.Int64("clientId", tc.auth.ClientId), + slog.Int64("registrationId", tc.auth.RegistrationId), ) return nil diff --git a/pkg/client/registration_management.go b/pkg/client/registration_management.go new file mode 100644 index 0000000..9b49afa --- /dev/null +++ b/pkg/client/registration_management.go @@ -0,0 +1,199 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "log/slog" + "os" + + "github.com/SUSE/telemetry/pkg/types" + "github.com/google/uuid" +) + +const ( + REGISTRATION_PATH = CONFIG_DIR + "/registration" +) + +type TelemetryClientRegistration struct { + types.ClientRegistration + path string + valid bool + no_retry bool +} + +func NewTelemetryClientRegistration() *TelemetryClientRegistration { + return &TelemetryClientRegistration{ + path: REGISTRATION_PATH, + valid: false, + no_retry: false, + } +} + +func (r *TelemetryClientRegistration) RetriesEnabled() bool { + return !r.no_retry +} + +func (r *TelemetryClientRegistration) DisableRetries() { + r.no_retry = true +} + +func (r *TelemetryClientRegistration) Valid() bool { + return r.valid +} + +func (r *TelemetryClientRegistration) Path() string { + return r.path +} + +// for testing purposes +func (r *TelemetryClientRegistration) SetPath(path string) { + r.path = path +} + +func (r *TelemetryClientRegistration) Accessible() bool { + if _, err := os.Open(r.path); err != nil { + return false + } + return true +} + +func (r *TelemetryClientRegistration) Generate() { + r.ClientId = uuid.New().String() + r.SystemUUID = getSystemUUID() + r.Timestamp = types.Now().String() + r.valid = true +} + +func (r *TelemetryClientRegistration) Registration() types.ClientRegistration { + return r.ClientRegistration +} + +func (r *TelemetryClientRegistration) String() string { + return fmt.Sprintf( + "", + r.path, + r.valid, + r.ClientRegistration.String(), + ) +} + +func (r *TelemetryClientRegistration) Save() (err error) { + // saving an invalid registration is not supported + if !r.valid { + return fmt.Errorf("client registration not valid; cannot save") + } + + // marshal the registration public fields as JSON + bytes, err := json.Marshal(r) + if err != nil { + slog.Error( + "failed to json.Marshal() client registration", + slog.String("reg", r.String()), + slog.String("err", err.Error()), + ) + return + } + + // save the JSON encoded registration fields + err = os.WriteFile(r.path, bytes, 0600) + if err != nil { + slog.Error( + "failed to write client registration file", + slog.String("reg", r.String()), + slog.String("err", err.Error()), + ) + return + } + + slog.Debug( + "client registration saved", + slog.String("reg", r.String()), + ) + return +} + +func (r *TelemetryClientRegistration) Load() (err error) { + // check that the specified path exists, failing appropriately + // if there are any issues os.Stat()ing the file. + _, err = os.Stat(r.path) + if err != nil { + var msg string + if errors.Is(err, fs.ErrNotExist) { + msg = "client registration file not found" + } else { + msg = "unable to os.Stat() client registration file" + } + slog.Error( + msg, + slog.String("regPath", r.path), + slog.String("err", err.Error()), + ) + return + } + + // retrieve the contents of the specified client registration file, + // failing if unable to do so. + bytes, err := os.ReadFile(r.path) + if err != nil { + slog.Error( + "failed to read client registration file", + slog.String("regPath", r.path), + slog.String("err", err.Error()), + ) + return + } + + // unmarshal the contents of the client registration file into the + // client registration structure + err = json.Unmarshal(bytes, r) + if err != nil { + slog.Error( + "failed to json.Unmarshal() client registration file contents", + slog.String("regPath", r.path), + slog.String("contents", string(bytes)), + slog.String("err", err.Error()), + ) + return + } + + slog.Debug( + "client registration loaded", + slog.String("reg", r.String()), + ) + return +} + +func (r *TelemetryClientRegistration) Remove() (err error) { + // mark in-memory version as invalid + r.valid = false + + // check if registration file exists + _, err = os.Stat(r.path) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + // nothing to do, and not a failure, if registration file doesn't exist + err = nil + } else { + slog.Error( + "unable to os.Stat() client registration file", + slog.String("regPath", r.path), + slog.String("err", err.Error()), + ) + } + return + } + + // remove the registration client, reporting any errors that occurr + err = os.Remove(r.path) + if err != nil { + slog.Error( + "failed to os.Remove() client registration file", + slog.String("regPath", r.path), + slog.String("err", err.Error()), + ) + } + + return +} diff --git a/pkg/client/report.go b/pkg/client/report.go index fa03b30..0a447bc 100644 --- a/pkg/client/report.go +++ b/pkg/client/report.go @@ -34,7 +34,7 @@ func (tc *TelemetryClient) submitReportInternal(report *telemetrylib.TelemetryRe req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", "Bearer "+tc.auth.Token.String()) - req.Header.Add("X-Telemetry-Client-Id", fmt.Sprintf("%d", tc.auth.ClientId)) + req.Header.Add("X-Telemetry-Registration-Id", fmt.Sprintf("%d", tc.auth.RegistrationId)) httpClient := http.DefaultClient resp, err := httpClient.Do(req) diff --git a/pkg/client/systemuuid.go b/pkg/client/systemuuid.go new file mode 100644 index 0000000..1a9cc3d --- /dev/null +++ b/pkg/client/systemuuid.go @@ -0,0 +1,41 @@ +package client + +import ( + "log/slog" + "os" +) + +const ( + // Path to Linux hardware UUID file under /sys + LINUX_SYSTEM_UUID_PATH = "/sys/class/dmi/id/product_uuid" +) + +// retrieve the system uuid +func getSystemUUID() string { + // TODO: determine appropriate environment specific system UUID path + sysuuidPath := LINUX_SYSTEM_UUID_PATH + + // if identified system UUID path doesn't exist, return empty string + if !checkFileExists(sysuuidPath) { + slog.Debug( + "Unable to locate the Linux hardware UUID", + slog.String("path", sysuuidPath), + ) + return "" + } + + // if identified system UUID path can't be read, return empty string + // NOTE: retrieving the contents may require superuser privileges. + uuid, err := os.ReadFile(sysuuidPath) + if err != nil { + slog.Debug( + "unable to retrieve the system UUID - superuser privs may be required", + slog.String("path", sysuuidPath), + slog.String("err", err.Error()), + ) + return "" + } + + // return the retrieved system uuid + return string(uuid) +} diff --git a/pkg/lib/bundles.go b/pkg/lib/bundles.go index e830823..463cc3c 100644 --- a/pkg/lib/bundles.go +++ b/pkg/lib/bundles.go @@ -15,7 +15,7 @@ type TelemetryBundle struct { Footer TelemetryBundleFooter `json:"footer" validate:"required"` } -func NewTelemetryBundle(clientId int64, customerId string, tags types.Tags) *TelemetryBundle { +func NewTelemetryBundle(clientId string, customerId string, tags types.Tags) *TelemetryBundle { tb := new(TelemetryBundle) // fill in header fields @@ -36,7 +36,7 @@ func NewTelemetryBundle(clientId int64, customerId string, tags types.Tags) *Tel type TelemetryBundleHeader struct { BundleId string `json:"bundleId" validate:"required"` BundleTimeStamp string `json:"bundleTimeStamp" validate:"required"` - BundleClientId int64 `json:"bundleClientId" validate:"required"` + BundleClientId string `json:"bundleClientId" validate:"required"` BundleCustomerId string `json:"buncleCustomerId" validate:"required"` BundleAnnotations []string `json:"bundleAnnotations"` } @@ -51,7 +51,7 @@ const bundlesColumns = `( id INTEGER NOT NULL PRIMARY KEY, bundleId VARCHAR(64) NOT NULL, bundleTimestamp VARCHAR(32) NOT NULL, - bundleClientId INTEGER NOT NULL, + bundleClientId VARCHAR NOT NULL, bundleCustomerId VARCHAR(64) NOT NULL, bundleAnnotations TEXT, bundleChecksum VARCHAR(256), @@ -66,14 +66,14 @@ type TelemetryBundleRow struct { Id int64 BundleId string BundleTimestamp string - BundleClientId int64 + BundleClientId string BundleCustomerId string BundleAnnotations string BundleChecksum string ReportId sql.NullInt64 } -func NewTelemetryBundleRow(clientId int64, customerId string, tags types.Tags) (*TelemetryBundleRow, error) { +func NewTelemetryBundleRow(clientId string, customerId string, tags types.Tags) (*TelemetryBundleRow, error) { bundle := NewTelemetryBundle(clientId, customerId, tags) bundleRow := new(TelemetryBundleRow) diff --git a/pkg/lib/processor.go b/pkg/lib/processor.go index 2d8b070..0dae1e7 100644 --- a/pkg/lib/processor.go +++ b/pkg/lib/processor.go @@ -21,14 +21,14 @@ type TelemetryProcessor interface { // Generate telemetry bundle GenerateBundle( - clientId int64, + clientId string, customerId string, tags types.Tags, ) (bundleRow *TelemetryBundleRow, err error) // Generate telemetry report GenerateReport( - clientId int64, + clientId string, tags types.Tags, ) (reportRow *TelemetryReportRow, err error) @@ -111,7 +111,7 @@ func (p *TelemetryProcessorImpl) AddData(telemetry types.TelemetryType, marshale return dataItemRow.Insert(p.t.storer.Conn) } -func (p *TelemetryProcessorImpl) GenerateBundle(clientId int64, customerId string, tags types.Tags) (bundleRow *TelemetryBundleRow, err error) { +func (p *TelemetryProcessorImpl) GenerateBundle(clientId string, customerId string, tags types.Tags) (bundleRow *TelemetryBundleRow, err error) { bundleRow, err = NewTelemetryBundleRow(clientId, customerId, tags) if err != nil { @@ -132,7 +132,7 @@ func (p *TelemetryProcessorImpl) GenerateBundle(clientId int64, customerId strin return } -func (p *TelemetryProcessorImpl) GenerateReport(clientId int64, tags types.Tags) (reportRow *TelemetryReportRow, err error) { +func (p *TelemetryProcessorImpl) GenerateReport(clientId string, tags types.Tags) (reportRow *TelemetryReportRow, err error) { reportRow, err = NewTelemetryReportRow(clientId, tags) if err != nil { diff --git a/pkg/lib/processor_test.go b/pkg/lib/processor_test.go index 6b26fd8..f29f871 100644 --- a/pkg/lib/processor_test.go +++ b/pkg/lib/processor_test.go @@ -128,7 +128,7 @@ func (t *TelemetryProcessorTestSuite) TestCreateBundle() { } btags := types.Tags{types.Tag("key1=value1"), types.Tag("key2")} - bundleRow, berr := telemetryprocessor.GenerateBundle(1, "customer id", btags) + bundleRow, berr := telemetryprocessor.GenerateBundle("1", "customer id", btags) if berr != nil { t.Fail("Test failed to create the bundle") @@ -206,7 +206,7 @@ func (t *TelemetryProcessorTestSuite) TestReport() { // generate a bundle to hold unassigned items btags := types.Tags{types.Tag("key1=value1"), types.Tag("key2")} - bundleRow, berr := telemetryprocessor.GenerateBundle(1, "customer id", btags) + bundleRow, berr := telemetryprocessor.GenerateBundle("1", "customer id", btags) if berr != nil { t.Fail("Test failed to create the bundle") } @@ -239,7 +239,7 @@ func (t *TelemetryProcessorTestSuite) TestReport() { // generate a second bundle btags1 := types.Tags{types.Tag("key3=value3"), types.Tag("key4")} - bundleRow, berr = telemetryprocessor.GenerateBundle(1, "customer id", btags1) + bundleRow, berr = telemetryprocessor.GenerateBundle("1", "customer id", btags1) if berr != nil { t.Fail("Test failed to create the bundle") } @@ -274,7 +274,7 @@ func (t *TelemetryProcessorTestSuite) TestReport() { // generate a report consuming available bundles rtags := types.Tags{types.Tag("key5=value5"), types.Tag("key6")} - reportRow, err := telemetryprocessor.GenerateReport(123456, rtags) + reportRow, err := telemetryprocessor.GenerateReport("123456", rtags) assert.NoError(t.T(), err, "Report failed") // validate the total and unassigned bundle counts diff --git a/pkg/lib/reports.go b/pkg/lib/reports.go index c168a84..5993fef 100644 --- a/pkg/lib/reports.go +++ b/pkg/lib/reports.go @@ -15,7 +15,7 @@ type TelemetryReport struct { Footer TelemetryReportFooter `json:"footer" validate:"required"` } -func NewTelemetryReport(clientId int64, tags types.Tags) *TelemetryReport { +func NewTelemetryReport(clientId string, tags types.Tags) *TelemetryReport { tr := new(TelemetryReport) // fill in header fields @@ -35,7 +35,7 @@ func NewTelemetryReport(clientId int64, tags types.Tags) *TelemetryReport { type TelemetryReportHeader struct { ReportId string `json:"reportId" validate:"required"` ReportTimeStamp string `json:"reportTimeStamp" validate:"required"` - ReportClientId int64 `json:"reportClientId" validate:"required"` + ReportClientId string `json:"reportClientId" validate:"required"` ReportAnnotations []string `json:"reportAnnotations"` } @@ -48,7 +48,7 @@ const reportsColumns = `( id INTEGER NOT NULL PRIMARY KEY, reportId VARCHAR(64) NOT NULL, reportTimestamp VARCHAR(32) NOT NULL, - reportClientId INTEGER NOT NULL, + reportClientId VARCHAR NOT NULL, reportAnnotations TEXT, reportChecksum VARCHAR(256) )` @@ -57,12 +57,12 @@ type TelemetryReportRow struct { Id int64 ReportId string ReportTimestamp string - ReportClientId int64 + ReportClientId string ReportAnnotations string ReportChecksum string } -func NewTelemetryReportRow(clientId int64, tags types.Tags) (*TelemetryReportRow, error) { +func NewTelemetryReportRow(clientId string, tags types.Tags) (*TelemetryReportRow, error) { report := NewTelemetryReport(clientId, tags) reportRow := TelemetryReportRow{} diff --git a/pkg/restapi/restapi.go b/pkg/restapi/restapi.go index d6eb01f..6c9c7b6 100644 --- a/pkg/restapi/restapi.go +++ b/pkg/restapi/restapi.go @@ -14,7 +14,7 @@ import ( // ClientRegistrationRequest is the request payload body POST'd to the server type ClientRegistrationRequest struct { - ClientInstanceId types.ClientInstanceId `json:"clientInstanceId"` + ClientRegistration types.ClientRegistration `json:"clientRegistration"` } func (c *ClientRegistrationRequest) String() string { @@ -25,7 +25,7 @@ func (c *ClientRegistrationRequest) String() string { // ClientRegistrationResponse is the response payload body from the server type ClientRegistrationResponse struct { - ClientId int64 `json:"clientId"` + RegistrationId int64 `json:"registrationId"` AuthToken string `json:"authToken"` RegistrationDate string `json:"registrationDate"` } @@ -38,8 +38,8 @@ func (c *ClientRegistrationResponse) String() string { // Client Authenticate handling via /temelemtry/authenticate type ClientAuthenticationRequest struct { - ClientId int64 `json:"clientId"` - InstIdHash types.ClientInstanceIdHash `json:"instIdHash"` + RegistrationId int64 `json:"registrationId"` + RegHash types.ClientRegistrationHash `json:"regHash"` } func (c *ClientAuthenticationRequest) String() string { diff --git a/pkg/types/types.go b/pkg/types/types.go index ec48992..1b27c63 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -129,31 +129,36 @@ func (tc *TelemetryClass) String() string { return "UNKNOWN_TELEMETRY_CLASS" } -type ClientInstanceIdHash struct { +type ClientRegistrationHash struct { Method string `json:"method"` Value string `json:"value"` } -func (c *ClientInstanceIdHash) String() string { +func (c *ClientRegistrationHash) String() string { bytes, _ := json.Marshal(c) return string(bytes) } -func (c *ClientInstanceIdHash) Match(m *ClientInstanceIdHash) bool { +func (c *ClientRegistrationHash) Match(m *ClientRegistrationHash) bool { return (c.Method == m.Method) && (c.Value == m.Value) } -// ClientInstanceId -type ClientInstanceId string +// ClientRegistration +type ClientRegistration struct { + ClientId string `json:"clientId"` + SystemUUID string `json:"systemUUID"` + Timestamp string `json:"timestamp"` +} -func (c *ClientInstanceId) String() string { - return string(*c) +func (c *ClientRegistration) String() string { + bytes, _ := json.Marshal(c) + return string(bytes) } const DEF_INSTID_HASH_METHOD = "sha256" -func (c *ClientInstanceId) Hash(inputMethod string) *ClientInstanceIdHash { +func (c *ClientRegistration) Hash(inputMethod string) *ClientRegistrationHash { var methodHash hash.Hash // this routine is expected to succeed so ensure a valid method is used @@ -179,10 +184,10 @@ func (c *ClientInstanceId) Hash(inputMethod string) *ClientInstanceIdHash { case "sha512": methodHash = sha512.New() } - methodHash.Write([]byte(*c)) + methodHash.Write([]byte(c.String())) // construct the return value - return &ClientInstanceIdHash{ + return &ClientRegistrationHash{ Method: method, Value: hex.EncodeToString(methodHash.Sum(nil)), } diff --git a/telemetry.go b/telemetry.go index 8fd262a..9b6dd80 100644 --- a/telemetry.go +++ b/telemetry.go @@ -142,7 +142,7 @@ const ( CLIENT_DISABLED CLIENT_MISCONFIGURED CLIENT_DATASTORE_ACCESSIBLE - CLIENT_INSTANCE_ID_ACCESSIBLE + CLIENT_REGISTRATION_ACCESSIBLE CLIENT_REGISTERED ) @@ -158,8 +158,8 @@ func (cs *ClientStatus) String() string { return "MISCONFIGURED" case CLIENT_DATASTORE_ACCESSIBLE: return "DATASTORE_ACCESSIBLE" - case CLIENT_INSTANCE_ID_ACCESSIBLE: - return "INSTANCE_ID_ACCESSIBLE" + case CLIENT_REGISTRATION_ACCESSIBLE: + return "REGISTRATION_ACCESSIBLE" case CLIENT_REGISTERED: return "REGISTERED" } @@ -204,14 +204,14 @@ func Status() (status ClientStatus) { // update status to indicate that telemetry client datastore is accessible status = CLIENT_DATASTORE_ACCESSIBLE - // check that an instance id is available - if !tc.InstanceIdAccessible() { - slog.Warn("Telemetry client instance id has not been setup", slog.String("path", tc.InstIdPath())) + // check that an registration is available + if !tc.RegistrationAccessible() { + slog.Warn("Telemetry client registration has not been setup", slog.String("path", tc.RegistrationPath())) return } - // update status to indicate client has instance id - status = CLIENT_INSTANCE_ID_ACCESSIBLE + // update status to indicate client has registration + status = CLIENT_REGISTRATION_ACCESSIBLE // check that we have obtained a telemetry auth token if !tc.AuthAccessible() {