diff --git a/v2/logging.go b/v2/logging.go index 5044972..6cf0c64 100644 --- a/v2/logging.go +++ b/v2/logging.go @@ -20,6 +20,7 @@ const ( LogstreamCriblEndpoint LogstreamEndpointType = "cribl" LogstreamDatadogEndpoint LogstreamEndpointType = "datadog" LogstreamAxiomEndpoint LogstreamEndpointType = "axiom" + LogstreamS3Endpoint LogstreamEndpointType = "s3" ) const ( @@ -27,20 +28,40 @@ const ( LogTypeNetwork LogType = "network" ) +const ( + S3AccessKeyAuthentication S3AuthenticationType = "accesskey" + S3RoleARNAuthentication S3AuthenticationType = "rolearn" +) + // LogstreamConfiguration type defines a log stream entity in tailscale. type LogstreamConfiguration struct { - LogType LogType `json:"logType,omitempty"` - DestinationType LogstreamEndpointType `json:"destinationType,omitempty"` - URL string `json:"url,omitempty"` - User string `json:"user,omitempty"` + LogType LogType `json:"logType,omitempty"` + DestinationType LogstreamEndpointType `json:"destinationType,omitempty"` + URL string `json:"url,omitempty"` + User string `json:"user,omitempty"` + S3Bucket string `json:"s3Bucket,omitempty"` + S3Region string `json:"s3Region,omitempty"` + S3KeyPrefix string `json:"s3KeyPrefix,omitempty"` + S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"` + S3AccessKeyID string `json:"s3AccessKeyId,omitempty"` + S3RoleARN string `json:"s3RoleArn,omitempty"` + S3ExternalID string `json:"s3ExternalId,omitempty"` } // SetLogstreamConfigurationRequest type defines a request for setting a LogstreamConfiguration. type SetLogstreamConfigurationRequest struct { - DestinationType LogstreamEndpointType `json:"destinationType,omitempty"` - URL string `json:"url,omitempty"` - User string `json:"user,omitempty"` - Token string `json:"token,omitempty"` + DestinationType LogstreamEndpointType `json:"destinationType,omitempty"` + URL string `json:"url,omitempty"` + User string `json:"user,omitempty"` + Token string `json:"token,omitempty"` + S3Bucket string `json:"s3Bucket,omitempty"` + S3Region string `json:"s3Region,omitempty"` + S3KeyPrefix string `json:"s3KeyPrefix,omitempty"` + S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"` + S3AccessKeyID string `json:"s3AccessKeyId,omitempty"` + S3SecretAccessKey string `json:"s3SecretAccessKey,omitempty"` + S3RoleARN string `json:"s3RoleArn,omitempty"` + S3ExternalID string `json:"s3ExternalId,omitempty"` } // LogstreamEndpointType describes the type of the endpoint. @@ -49,6 +70,9 @@ type LogstreamEndpointType string // LogType describes the type of logging. type LogType string +// S3AuthenticationType describes the type of authentication used to stream logs to a LogstreamS3Endpoint. +type S3AuthenticationType string + // LogstreamConfiguration retrieves the tailnet's [LogstreamConfiguration] for the given [LogType]. func (lr *LoggingResource) LogstreamConfiguration(ctx context.Context, logType LogType) (*LogstreamConfiguration, error) { req, err := lr.buildRequest(ctx, http.MethodGet, lr.buildTailnetURL("logging", logType, "stream")) @@ -78,3 +102,35 @@ func (lr *LoggingResource) DeleteLogstreamConfiguration(ctx context.Context, log return lr.do(req, nil) } + +// AWSExternalID represents an AWS External ID that Tailscale can use to stream logs from a +// particular Tailscale AWS account to a LogstreamS3Endpoint that uses S3RoleARNAuthentication. +type AWSExternalID struct { + ExternalID string `json:"externalId,omitempty"` + TailscaleAWSAccountID string `json:"tailscaleAwsAccountId,omitempty"` +} + +// CreateOrGetAwsExternalId gets an AWS External ID that Tailscale can use to stream logs to +// a LogstreamS3Endpoint using S3RoleARNAuthentication, creating a new one for this tailnet +// when necessary. +func (lr *LoggingResource) CreateOrGetAwsExternalId(ctx context.Context, reusable bool) (*AWSExternalID, error) { + req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id"), requestBody(map[string]bool{ + "reusable": reusable, + })) + if err != nil { + return nil, err + } + return body[AWSExternalID](lr, req) +} + +// ValidateAWSTrustPolicy validates that Tailscale can assume your AWS IAM role with (and only +// with) the given AWS External ID. +func (lr *LoggingResource) ValidateAWSTrustPolicy(ctx context.Context, awsExternalID string, roleARN string) error { + req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id", awsExternalID, "validate-aws-trust-policy"), requestBody(map[string]string{ + "roleArn": roleARN, + })) + if err != nil { + return err + } + return lr.do(req, nil) +} diff --git a/v2/logging_test.go b/v2/logging_test.go index f741157..9b4b8ae 100644 --- a/v2/logging_test.go +++ b/v2/logging_test.go @@ -36,10 +36,18 @@ func TestClient_SetLogstreamConfiguration(t *testing.T) { server.ResponseCode = http.StatusOK logstreamRequest := tsclient.SetLogstreamConfigurationRequest{ - DestinationType: tsclient.LogstreamCriblEndpoint, - URL: "http://example.com", - User: "my-user", - Token: "my-token", + DestinationType: tsclient.LogstreamCriblEndpoint, + URL: "http://example.com", + User: "my-user", + Token: "my-token", + S3Bucket: "my-bucket", + S3Region: "us-west-2", + S3KeyPrefix: "logs/", + S3AuthenticationType: tsclient.S3AccessKeyAuthentication, + S3AccessKeyID: "my-access-key-id", + S3SecretAccessKey: "my-secret-access-key", + S3RoleARN: "my-role-arn", + S3ExternalID: "my-external-id", } server.ResponseBody = nil @@ -64,3 +72,46 @@ func TestClient_DeleteLogstream(t *testing.T) { assert.Equal(t, http.MethodDelete, server.Method) assert.Equal(t, "/api/v2/tailnet/example.com/logging/configuration/stream", server.Path) } + +func TestClient_CreateOrGetAwsExternalId(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + wantExternalID := &tsclient.AWSExternalID{ + ExternalID: "external-id", + TailscaleAWSAccountID: "account-id", + } + server.ResponseBody = wantExternalID + + gotExternalID, err := client.Logging().CreateOrGetAwsExternalId(context.Background(), true) + assert.NoError(t, err) + assert.Equal(t, server.Method, http.MethodPost) + assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id") + assert.Equal(t, gotExternalID, wantExternalID) + + gotRequest := make(map[string]bool) + err = json.Unmarshal(server.Body.Bytes(), &gotRequest) + assert.NoError(t, err) + assert.EqualValues(t, gotRequest, map[string]bool{"reusable": true}) +} + +func TestClient_ValidateAWSTrustPolicy(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + roleARN := "arn:aws:iam::123456789012:role/example-role" + + err := client.Logging().ValidateAWSTrustPolicy(context.Background(), "external-id-0000-0000", roleARN) + assert.NoError(t, err) + assert.Equal(t, server.Method, http.MethodPost) + assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id/external-id-0000-0000/validate-aws-trust-policy") + + gotRequest := make(map[string]string) + err = json.Unmarshal(server.Body.Bytes(), &gotRequest) + assert.NoError(t, err) + assert.EqualValues(t, gotRequest, map[string]string{"roleArn": roleARN}) +}