diff --git a/baton_capabilities.json b/baton_capabilities.json new file mode 100644 index 00000000..e261c031 --- /dev/null +++ b/baton_capabilities.json @@ -0,0 +1,78 @@ +{ + "@type": "type.googleapis.com/c1.connector.v2.ConnectorCapabilities", + "resourceTypeCapabilities": [ + { + "resourceType": { + "id": "database", + "displayName": "Database" + }, + "capabilities": [ + "CAPABILITY_SYNC", + "CAPABILITY_PROVISION" + ], + "permissions": {} + }, + { + "resourceType": { + "id": "routine", + "displayName": "Routine" + }, + "capabilities": [ + "CAPABILITY_SYNC", + "CAPABILITY_PROVISION" + ], + "permissions": {} + }, + { + "resourceType": { + "id": "server", + "displayName": "Server" + }, + "capabilities": [ + "CAPABILITY_SYNC", + "CAPABILITY_PROVISION" + ], + "permissions": {} + }, + { + "resourceType": { + "id": "table", + "displayName": "Table" + }, + "capabilities": [ + "CAPABILITY_SYNC", + "CAPABILITY_PROVISION" + ], + "permissions": {} + }, + { + "resourceType": { + "id": "user", + "displayName": "User", + "traits": [ + "TRAIT_USER" + ] + }, + "capabilities": [ + "CAPABILITY_SYNC", + "CAPABILITY_ACCOUNT_PROVISIONING", + "CAPABILITY_RESOURCE_DELETE" + ], + "permissions": {} + } + ], + "connectorCapabilities": [ + "CAPABILITY_PROVISION", + "CAPABILITY_SYNC", + "CAPABILITY_ACCOUNT_PROVISIONING", + "CAPABILITY_RESOURCE_DELETE" + ], + "credentialDetails": { + "capabilityAccountProvisioning": { + "supportedCredentialOptions": [ + "CAPABILITY_DETAIL_CREDENTIAL_OPTION_RANDOM_PASSWORD" + ], + "preferredCredentialOption": "CAPABILITY_DETAIL_CREDENTIAL_OPTION_RANDOM_PASSWORD" + } + } +} \ No newline at end of file diff --git a/config_schema.json b/config_schema.json new file mode 100644 index 00000000..f01944b4 --- /dev/null +++ b/config_schema.json @@ -0,0 +1,126 @@ +{ + "fields": [ + { + "name": "log-level", + "description": "The log level: debug, info, warn, error", + "isOps": true, + "stringField": { + "defaultValue": "info" + } + }, + { + "name": "log-level-debug-expires-at", + "description": "The timestamp indicating when debug-level logging should expire", + "isOps": true, + "stringField": {} + }, + { + "name": "session-store-maximum-size", + "description": "The maximum size of the local in-memory session store cache in bytes.", + "isOps": true, + "intField": { + "defaultValue": "15728640" + } + }, + { + "name": "otel-collector-endpoint", + "description": "The endpoint of the OpenTelemetry collector to send observability data to (used for both tracing and logging if specific endpoints are not provided)", + "isOps": true, + "stringField": {} + }, + { + "name": "otel-collector-endpoint-tls-cert-path", + "description": "Path to a file containing a PEM-encoded certificate to use as a CA for TLS connections to the OpenTelemetry collector", + "isOps": true, + "stringField": {} + }, + { + "name": "otel-collector-endpoint-tls-cert", + "description": "A PEM-encoded certificate to use as a CA for TLS connections to the OpenTelemetry collector", + "isOps": true, + "stringField": {} + }, + { + "name": "otel-collector-endpoint-tls-insecure", + "description": "Allow insecure connections to the OpenTelemetry collector", + "isOps": true, + "boolField": {} + }, + { + "name": "otel-tracing-disabled", + "description": "Disable OpenTelemetry tracing", + "isOps": true, + "boolField": {} + }, + { + "name": "otel-logging-disabled", + "description": "Disable OpenTelemetry logging", + "isOps": true, + "boolField": {} + }, + { + "name": "health-check", + "description": "Enable the HTTP health check endpoint", + "isOps": true, + "boolField": {} + }, + { + "name": "health-check-port", + "description": "Port for the HTTP health check endpoint", + "isOps": true, + "intField": { + "defaultValue": "8081" + } + }, + { + "name": "health-check-bind-address", + "description": "Bind address for health check server (127.0.0.1 for localhost-only)", + "isOps": true, + "stringField": { + "defaultValue": "127.0.0.1" + } + }, + { + "name": "http-timeout-seconds", + "description": "HTTP client timeout in seconds (max 1800)", + "isOps": true, + "intField": { + "defaultValue": "300", + "rules": { + "lte": "1800", + "gte": "1" + } + } + }, + { + "name": "connection-string", + "description": "The connection string for connecting to MySQL ($BATON_CONNECTION_STRING)", + "isRequired": true, + "isSecret": true, + "stringField": { + "rules": { + "isRequired": true + } + } + }, + { + "name": "skip-database", + "description": "Skip syncing privileges from these databases ($BATON_SKIP_DATABASE)", + "stringSliceField": { + "rules": {} + } + }, + { + "name": "expand-columns", + "description": "Provide a table like db.table to expand the column privileges into their own entitlements. $(BATON_EXPAND_COLUMNS)", + "stringSliceField": { + "rules": {} + } + }, + { + "name": "collapse-users", + "description": "Combine user@host pairs into a single user@[hosts...] identity $(BATON_COLLAPSE_USERS)", + "boolField": {} + } + ] +} \ No newline at end of file diff --git a/docs/connector.mdx b/docs/connector.mdx new file mode 100644 index 00000000..2894cc11 --- /dev/null +++ b/docs/connector.mdx @@ -0,0 +1,209 @@ +--- +title: "Set up a MySQL connector" +og:title: "Set up a MySQL connector" +description: "ConductorOne provides identity governance for MySQL. Integrate your MySQL instance with ConductorOne to run user access reviews (UARs), enable just-in-time access requests, and automatically provision and deprovision access." +og:description: "ConductorOne provides identity governance for MySQL. Integrate your MySQL instance with ConductorOne to run user access reviews (UARs), enable just-in-time access requests, and automatically provision and deprovision access." +sidebarTitle: "MySQL" +--- + +{/* AUTO-GENERATED:START - capabilities + Generated from baton_capabilities.json. Do not edit manually. */} + +## Capabilities + +The MySQL connector syncs the following resources: + +| Resource | Sync | Provision | +| :--- | :--- | :--- | +| Databases | | Grant, Revoke | +| Routines | | Grant, Revoke | +| Servers | | Grant, Revoke | +| Tables | | Grant, Revoke | +| Users | | Create, Delete | + +{/* AUTO-GENERATED:END - capabilities */} + +**Notes:** + +- Roles are synced and their privilege assignments can be granted and revoked on **MySQL 8+** only. The role resource type is enabled automatically when a MySQL 8 server is detected. +- Columns are synced as sub-resources of tables when the `--expand-columns` flag is configured. + +## Gather MySQL credentials + + + The ConductorOne connector requires a dedicated MySQL user with `SELECT` + privileges on the `mysql` system database tables that contain privilege + information. Creating a least-privilege user is strongly recommended over + using a root account. + + +### Create a MySQL connector user + + + + Log in to your MySQL server using an account with `GRANT` privileges (for example, `root`). + + + Create a dedicated user for the connector: + + ```sql + CREATE USER conductorone IDENTIFIED BY 'secure-password'; + ``` + + + Grant the minimum required privileges. Choose the block that matches your MySQL version. + + **MySQL 5.7:** + + ```sql + GRANT SELECT (Host, User, Db, Select_priv, Insert_priv, Update_priv, Delete_priv, + Create_priv, Drop_priv, Grant_priv, References_priv, Index_priv, Alter_priv, + Create_tmp_table_priv, Lock_tables_priv, Execute_priv, Create_view_priv, + Show_view_priv, Create_routine_priv, Alter_routine_priv, Event_priv, + Trigger_priv) ON mysql.db TO conductorone; + GRANT SELECT (Host, User, Db, Table_priv, Table_name) ON mysql.tables_priv TO conductorone; + GRANT SELECT (Host, User, Db, Column_name, Column_priv, Table_name) ON mysql.columns_priv TO conductorone; + GRANT SELECT (Host, User, Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, + Drop_priv, Reload_priv, Shutdown_priv, Process_priv, References_priv, Index_priv, + Alter_priv, Show_db_priv, Super_priv, Create_tmp_table_priv, Lock_tables_priv, + Execute_priv, Repl_slave_priv, Repl_client_priv, Create_view_priv, Show_view_priv, + Create_routine_priv, Alter_routine_priv, Create_user_priv, Event_priv, Trigger_priv, + Create_tablespace_priv, File_priv, Grant_priv, authentication_string) ON mysql.user TO conductorone; + ``` + + **MySQL 8+:** + + ```sql + GRANT SELECT (USER, HOST, PRIV, WITH_GRANT_OPTION) ON mysql.global_grants TO conductorone; + GRANT SELECT (Host, User, Db, Select_priv, Insert_priv, Update_priv, Delete_priv, + Create_priv, Drop_priv, Grant_priv, References_priv, Index_priv, Alter_priv, + Create_tmp_table_priv, Lock_tables_priv, Execute_priv, Create_view_priv, + Show_view_priv, Create_routine_priv, Alter_routine_priv, Event_priv, + Trigger_priv) ON mysql.db TO conductorone; + GRANT SELECT (Host, User, Db, Table_priv, Table_name) ON mysql.tables_priv TO conductorone; + GRANT SELECT (Host, User, Db, Column_name, Column_priv, Table_name) ON mysql.columns_priv TO conductorone; + GRANT SELECT (Host, User, Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, + Drop_priv, Reload_priv, Shutdown_priv, Process_priv, References_priv, Index_priv, + Alter_priv, Show_db_priv, Super_priv, Create_tmp_table_priv, Lock_tables_priv, + Execute_priv, Repl_slave_priv, Repl_client_priv, Create_view_priv, Show_view_priv, + Create_routine_priv, Alter_routine_priv, Create_user_priv, Event_priv, Trigger_priv, + Create_tablespace_priv, Create_role_priv, Drop_role_priv, File_priv, Grant_priv, + authentication_string) ON mysql.user TO conductorone; + GRANT SELECT (FROM_HOST, FROM_USER, TO_HOST, TO_USER, WITH_ADMIN_OPTION) ON mysql.role_edges TO conductorone; + ``` + + + Grant `SELECT` on all databases so the connector can introspect schemas: + + ```sql + GRANT SELECT ON *.* TO conductorone; + ``` + + + Note the connection string in DSN format — you will need it when configuring the connector: + + ``` + conductorone:secure-password@tcp(your-mysql-host:3306)/ + ``` + + + +That's it! You now have the credentials needed to configure the connector. + +## Configure the MySQL connector + + + You must have the **Connector Administrator** or **Super Administrator** role in ConductorOne to add a connector. MySQL is a self-hosted connector — you run the Baton binary in your own infrastructure. You will need: + + - MySQL connection string (`user:password@tcp(host:port)/`) + - A C1 Client ID and Client Secret (obtained in Step 1 below) + + +### Resources + +- Binary and packages: [https://dist.conductorone.com/ConductorOne/baton-mysql](https://dist.conductorone.com/ConductorOne/baton-mysql) +- Source code: [https://github.com/conductorone/baton-mysql](https://github.com/conductorone/baton-mysql) + +### Step 1 + +Add a Baton connector in ConductorOne and rotate credentials to get a C1 Client ID and Client Secret. + + + + In ConductorOne, go to **Connectors** → **Add connector** → **Baton**. + + + Name the connector and set an owner, then save. + + + On the connector detail page, click **Rotate credentials** and copy the **Client ID** and **Client Secret**. + + + +### Step 2 + +Deploy the connector using Kubernetes. Create the following manifests: + +{/* AUTO-GENERATED:START - config-params + Generated from config_schema.json. Do not edit manually. */} + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: baton-mysql-secret +type: Opaque +stringData: + BATON_CLIENT_ID: "" + BATON_CLIENT_SECRET: "" + BATON_CONNECTION_STRING: "" + # Optional: + # BATON_SKIP_DATABASE: "performance_schema,information_schema" + # BATON_EXPAND_COLUMNS: "mydb.mytable,otherdb.othertable" + # BATON_COLLAPSE_USERS: "true" +``` + +The supported environment variables are: + +- **BATON_CONNECTION_STRING** (required): MySQL connection string in DSN format, e.g. `user:password@tcp(host:3306)/` +- **BATON_SKIP_DATABASE**: Comma-separated list of database names to exclude from the sync. +- **BATON_EXPAND_COLUMNS**: Comma-separated list of tables (in `db.table` format) whose column-level privileges should be surfaced as individual entitlements. +- **BATON_COLLAPSE_USERS**: When `true`, combines `user@host` pairs that share the same username into a single identity. + +{/* AUTO-GENERATED:END - config-params */} + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: baton-mysql +spec: + replicas: 1 + selector: + matchLabels: + app: baton-mysql + template: + metadata: + labels: + app: baton-mysql + spec: + containers: + - name: baton-mysql + image: public.ecr.aws/conductorone/baton-mysql:latest + envFrom: + - secretRef: + name: baton-mysql-secret +``` + +### Step 3 + +Apply the manifests and verify the connector appears as **Connected** in ConductorOne. + +```bash +kubectl apply -f baton-mysql-secret.yaml +kubectl apply -f baton-mysql-deployment.yaml +``` + + + Download the `baton-mysql` binary and packages from the [ConductorOne distribution center](https://dist.conductorone.com/ConductorOne/baton-mysql). + diff --git a/docs/docs-info.md b/docs/docs-info.md new file mode 100644 index 00000000..17d8cb75 --- /dev/null +++ b/docs/docs-info.md @@ -0,0 +1,59 @@ +# Baton MySQL - Connector Documentation + +This document provides information needed to set up and use the connector. + +## Connector Capabilities + +### 1. What resources does the connector sync? + +| Resource | Description | +|----------|-------------| +| **Server** | The MySQL server instance itself; root of the resource hierarchy | +| **Database** | Individual databases hosted on the server; scoped privilege assignments | +| **Table** | Tables within databases; table-level privilege grants | +| **Routine** | Stored procedures and functions; execute/alter privilege grants | +| **User** | MySQL accounts (`user@host`); principal identities in the access graph | +| **Role** | Named role objects (MySQL 8+ only); role-to-user assignment grants | +| **Column** | Individual table columns (optional, requires `--expand-columns`); column-level privilege grants | + +### 2. Can the connector provision any resources? If so, which ones? + +Yes. + +| Resource | Grant | Revoke | Create | Delete | +|----------|-------|--------|--------|--------| +| **Database privileges** | ✅ Grants a privilege on a database to a user | ✅ Revokes a privilege on a database from a user | - | - | +| **Table privileges** | ✅ Grants a privilege on a table to a user | ✅ Revokes a privilege on a table from a user | - | - | +| **Routine privileges** | ✅ Grants a privilege on a routine to a user | ✅ Revokes a privilege on a routine from a user | - | - | +| **Role assignments** | ✅ Assigns a role to a user (MySQL 8+ only) | ✅ Removes a role from a user (MySQL 8+ only) | - | - | +| **User accounts** | - | - | ✅ Creates a new MySQL user with a generated password | ✅ Drops a MySQL user account | + +## Connector Credentials + +### 1. What credentials or information are needed to set up the connector? + +| Credential | Required | Description | +|------------|----------|-------------| +| **connection-string** | Yes | MySQL DSN in Go driver format: `user:password@tcp(host:port)/` | +| **skip-database** | No | Comma-separated list of database names to exclude from sync | +| **expand-columns** | No | Tables (`db.table`) whose column-level privileges are expanded into individual entitlements | +| **collapse-users** | No | Combines `user@host` accounts with the same username into a single identity (default: false) | + +### 2. How are these credentials obtained? + +Create a dedicated MySQL user and grant it the minimum read privileges on `mysql` system tables. See `README.md` (Advanced Setup section) for the exact `GRANT` statements for MySQL 5.7 and MySQL 8+. + +The connection string format is: `username:password@tcp(hostname:3306)/` + +## Additional Notes + +### MySQL Version Requirements + +- **MySQL 5.7 and 8.x** are supported. +- Role sync and role grant/revoke are available on **MySQL 8+** only. The connector detects the server version at runtime and enables role support automatically. + +### API Documentation Links + +- [MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/) +- [MySQL 5.7 Reference Manual](https://dev.mysql.com/doc/refman/5.7/en/) +- [Go MySQL Driver DSN format](https://github.com/go-sql-driver/mysql#dsn-data-source-name) diff --git a/pkg/client/client.go b/pkg/client/client.go index 715cdee0..6dd3f865 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -47,12 +47,12 @@ func (t dbResourceID) Column() dbResourceID { func (t dbResourceID) SQLString() (string, error) { var sb strings.Builder - _, err := sb.WriteString(fmt.Sprintf("`%s`", t.DatabaseName)) + _, err := fmt.Fprintf(&sb, "`%s`", t.DatabaseName) if err != nil { return "", err } if t.ResourceName != "" && (t.ResourceTypeID == TableType || t.ResourceTypeID == ColumnType) { - _, err = sb.WriteString(fmt.Sprintf(".`%s`", t.ResourceName)) + _, err = fmt.Fprintf(&sb, ".`%s`", t.ResourceName) if err != nil { return "", err } @@ -63,15 +63,13 @@ func (t dbResourceID) SQLString() (string, error) { func (t dbResourceID) String() string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("%s:%s", t.ResourceTypeID, t.DatabaseName)) + fmt.Fprintf(&sb, "%s:%s", t.ResourceTypeID, t.DatabaseName) if t.ResourceName != "" { - sb.WriteString(".") - sb.WriteString(t.ResourceName) + fmt.Fprintf(&sb, ".%s", t.ResourceName) if t.SubResourceName != "" { - sb.WriteString(".") - sb.WriteString(t.SubResourceName) + fmt.Fprintf(&sb, ".%s", t.SubResourceName) } } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index e6b43c42..845c8bc6 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -286,18 +286,18 @@ func Test_generateRandomGrants(t *testing.T) { _, _ = grantStatements.WriteString(", ") } - _, _ = grantStatements.WriteString(fmt.Sprintf(`%s (`, p)) + _, _ = fmt.Fprintf(&grantStatements, "%s (", p) for jj, col := range g.resourceIDs { if jj != 0 { _, _ = grantStatements.WriteString(", ") } - _, _ = grantStatements.WriteString(fmt.Sprintf("`%s`", col.SubResourceName)) + _, _ = fmt.Fprintf(&grantStatements, "`%s`", col.SubResourceName) } _, _ = grantStatements.WriteString(")") } resourceString, err := g.resourceIDs[0].SQLString() require.NoError(t, err) - _, _ = grantStatements.WriteString(fmt.Sprintf(" ON %s TO %s;", resourceString, accountID)) + _, _ = fmt.Fprintf(&grantStatements, " ON %s TO %s;", resourceString, accountID) _ = grantStatements.WriteByte('\n') case TableType: @@ -318,7 +318,7 @@ func Test_generateRandomGrants(t *testing.T) { } resourceString, err := g.resourceIDs[0].SQLString() require.NoError(t, err) - _, _ = grantStatements.WriteString(fmt.Sprintf(" ON %s TO %s;\n", resourceString, accountID)) + _, _ = fmt.Fprintf(&grantStatements, " ON %s TO %s;\n", resourceString, accountID) case DatabaseType: privs := randomPrivs(r, dbPrivs(), privCount) @@ -338,7 +338,7 @@ func Test_generateRandomGrants(t *testing.T) { } resourceString, err := g.resourceIDs[0].SQLString() require.NoError(t, err) - _, _ = grantStatements.WriteString(fmt.Sprintf(" ON %s.* TO %s;\n", resourceString, accountID)) + _, _ = fmt.Fprintf(&grantStatements, " ON %s.* TO %s;\n", resourceString, accountID) default: require.NoError(t, fmt.Errorf("invalid resource type for grant item")) } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 4bcffb4c..5ac36368 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -79,7 +79,7 @@ func (c *Connector) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso newUserSyncer(c.client, c.skipDbs, c.expandCols, c.collapseUsers), } - if c.client.IsVersion8() { + if c.client != nil && c.client.IsVersion8() { syncers = append(syncers, newRoleSyncer(c.client, c.skipDbs, c.expandCols)) }