diff --git a/v2/logging.go b/v2/logging.go index 5044972..08e6a9f 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,41 @@ 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"` +} + +// CreateOrGetAwsExternalIdRequest is a request to create/get an AWS External ID from Tailscale. +type CreateOrGetAwsExternalIdRequest struct { + Reusable bool `json:"reusable,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, request CreateOrGetAwsExternalIdRequest) (*AWSExternalID, error) { + req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id"), requestBody(request)) + if err != nil { + return nil, err + } + return body[AWSExternalID](lr, req) +} + +// ValidateAWSTrustPolicyRequest is a request to validate that Tailscale can assume your AWS IAM role. +type ValidateAWSTrustPolicyRequest struct { + RoleARN string `json:"roleArn,omitempty"` +} + +// 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, request ValidateAWSTrustPolicyRequest) error { + req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id", awsExternalID, "validate-aws-trust-policy"), requestBody(request)) + if err != nil { + return err + } + return lr.do(req, nil) +} diff --git a/v2/logging_test.go b/v2/logging_test.go index f741157..92a7bfd 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 + + expectedExternalID := &tsclient.AWSExternalID{ + ExternalID: "external-id", + TailscaleAWSAccountID: "account-id", + } + server.ResponseBody = expectedExternalID + + request := tsclient.CreateOrGetAwsExternalIdRequest{ + Reusable: true, + } + + actualExternalID, err := client.Logging().CreateOrGetAwsExternalId(context.Background(), request) + assert.NoError(t, err) + assert.Equal(t, http.MethodPost, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/aws-external-id", server.Path) + assert.Equal(t, expectedExternalID, actualExternalID) +} + +func TestClient_ValidateAWSTrustPolicy(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + request := tsclient.ValidateAWSTrustPolicyRequest{ + RoleARN: "arn:aws:iam::123456789012:role/example-role", + } + + err := client.Logging().ValidateAWSTrustPolicy(context.Background(), "external-id-0000-0000", request) + assert.NoError(t, err) + assert.Equal(t, http.MethodPost, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/aws-external-id/external-id-0000-0000/validate-aws-trust-policy", server.Path) + var receivedRequest tsclient.ValidateAWSTrustPolicyRequest + err = json.Unmarshal(server.Body.Bytes(), &receivedRequest) + assert.NoError(t, err) + assert.EqualValues(t, request, receivedRequest) +}