Skip to content

Commit 20f361d

Browse files
authored
Merge pull request #17 from ConductorOne/AddAccountAndEntiltementProvision
[BB-747]Add Account & Entitlement Provision with Account Deprovision
2 parents 243521d + 6521c47 commit 20f361d

File tree

31 files changed

+1251
-66
lines changed

31 files changed

+1251
-66
lines changed

docs/images/docs-info.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
While developing the connector, please fill out this form. This information is needed to write docs and to help other users set up the connector.
2+
3+
## Connector capabilities
4+
5+
1. What resources does the connector sync?
6+
7+
-Users
8+
-Tables
9+
-Servers
10+
-Routines
11+
-Roles
12+
-Databases
13+
-Columns(optional)
14+
15+
2. Can the connector provision any resources? If so, which ones?
16+
17+
-Account provision and deprovision
18+
-Entitlement provision(Grant & Revoke) for all others resources
19+
20+
21+
## Connector credentials
22+
23+
1. What credentials or information are needed to set up the connector? (For example, API key, client ID and secret, domain, etc.)
24+
25+
- --connection-string string The connection string for connecting to MySQL, for example: "baton:baton-password@tcp(127.0.0.1:3306)/"
26+
- --expand-columns strings Provide a table like db.table to expand the column privileges into their own entitlements. This is optional,
27+
for example: baton_db.empleados
28+
29+
2. For each item in the list above:
30+
31+
* How does a user create or look up that credential or info? Please include links to (non-gated) documentation, screenshots (of the UI or of gated docs), or a video of the process.
32+
33+
-Customers can create a dedicated MySQL user with appropriate privileges using the following SQL:
34+
CREATE USER 'baton'@'%' IDENTIFIED BY 'baton-password';
35+
GRANT SELECT ON *.* TO 'baton'@'%'; -- for sync only
36+
GRANT ALL PRIVILEGES ON *.* TO 'baton'@'%'; -- for sync and provision
37+
38+
https://dev.mysql.com/doc/refman/8.0/en/create-user.html
39+
https://dev.mysql.com/doc/refman/8.0/en/grant.html
40+
41+
42+
* Does the credential need any specific scopes or permissions? If so, list them here.
43+
44+
-Yes:
45+
46+
For sync: SELECT privileges on mysql.*, information_schema.*, and all databases/tables you want to sync.
47+
48+
For provisioning: GRANT OPTION and ALL PRIVILEGES (or at least GRANT, SELECT, INSERT, UPDATE, DELETE, REFERENCES) on the target resources.
49+
50+
* If applicable: Is the list of scopes or permissions different to sync (read) versus provision (read-write)? If so, list the difference here.
51+
52+
-| Purpose | Required Privileges |
53+
| --------- | ------------------------------------------------------------- |
54+
| Sync | `SELECT` on `information_schema`, `mysql`, target DBs |
55+
| Provision | `GRANT OPTION`, `CREATE USER`, `DROP USER`, `GRANT`, `REVOKE` |
56+
57+
58+
* What level of access or permissions does the user need in order to create the credentials? (For example, must be a super administrator, must have access to the admin console, etc.)
59+
60+
-The credential must be created by a MySQL user with CREATE USER and GRANT OPTION privileges — typically a DBA or superuser (root or similar).
61+

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.4
55
toolchain go1.23.9
66

77
require (
8-
github.com/conductorone/baton-sdk v0.3.9
8+
github.com/conductorone/baton-sdk v0.3.10
99
github.com/go-sql-driver/mysql v1.7.0
1010
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
1111
github.com/jmoiron/sqlx v1.3.5

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
5858
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
5959
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
6060
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
61-
github.com/conductorone/baton-sdk v0.3.9 h1:D0YiYtRkpRByYsctlREqNG9pb5QAU5DW7sBlccAd3tI=
62-
github.com/conductorone/baton-sdk v0.3.9/go.mod h1:lWZHgu025Rsgs5jvBrhilGti0zWF2+YfaFY/bWOS/g0=
61+
github.com/conductorone/baton-sdk v0.3.10 h1:e1J0Y2knHTbzn9bAsjElmhv5lRpYwI6ixw0Ak+gx0JY=
62+
github.com/conductorone/baton-sdk v0.3.10/go.mod h1:lWZHgu025Rsgs5jvBrhilGti0zWF2+YfaFY/bWOS/g0=
6363
github.com/conductorone/dpop v0.2.3 h1:s91U3845GHQ6P6FWrdNr2SEOy1ES/jcFs1JtKSl2S+o=
6464
github.com/conductorone/dpop v0.2.3/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s=
6565
github.com/conductorone/dpop/integrations/dpop_grpc v0.2.3 h1:kLMCNIh0Mo2vbvvkCmJ3ixsPbXEJ6HPcW53Ku9yje3s=

pkg/client/columns.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67
"strings"
78

@@ -81,3 +82,74 @@ func (c *Client) ListColumns(ctx context.Context, parentResourceID *v2.ResourceI
8182

8283
return ret, nextPageToken, nil
8384
}
85+
86+
// If the privilege is "grant", it grants SELECT, INSERT, UPDATE, and REFERENCES privileges.
87+
func (c *Client) GrantColumnPrivilege(ctx context.Context, table string, column string, user string, privilege string) error {
88+
userSplit := strings.Split(user, "@")
89+
if len(userSplit) != 2 {
90+
return fmt.Errorf("invalid user format: %s", user)
91+
}
92+
userGrant := fmt.Sprintf("%s'@'%s", userSplit[0], userSplit[1])
93+
94+
var privileges []string
95+
if strings.ToLower(privilege) == "grant" {
96+
privileges = []string{"SELECT", "INSERT", "UPDATE", "REFERENCES"}
97+
} else {
98+
privileges = []string{strings.ToUpper(privilege)}
99+
}
100+
101+
escapedTable, err := escapeMySQLIdent(table)
102+
if err != nil {
103+
return err
104+
}
105+
escapedColumn, err := escapeMySQLIdent(column)
106+
if err != nil {
107+
return err
108+
}
109+
110+
var privilegeClauses []string
111+
for _, priv := range privileges {
112+
privilegeClauses = append(privilegeClauses, fmt.Sprintf("%s (%s)", priv, escapedColumn))
113+
}
114+
privilegesSQL := strings.Join(privilegeClauses, ", ")
115+
116+
query := fmt.Sprintf("GRANT %s ON %s TO '%s'", privilegesSQL, escapedTable, userGrant)
117+
118+
_ = c.db.MustExec(query)
119+
return nil
120+
}
121+
122+
func (c *Client) RevokeColumnPrivilege(ctx context.Context, table string, column string, user string, privilege string) error {
123+
userSplit := strings.Split(user, "@")
124+
if len(userSplit) != 2 {
125+
return fmt.Errorf("invalid user format: %s", user)
126+
}
127+
userRevoke := fmt.Sprintf("%s'@'%s", userSplit[0], userSplit[1])
128+
129+
var privileges []string
130+
if strings.ToLower(privilege) == "grant" {
131+
privileges = []string{"SELECT", "INSERT", "UPDATE", "REFERENCES"}
132+
} else {
133+
privileges = []string{strings.ToUpper(privilege)}
134+
}
135+
136+
escapedTable, err := escapeMySQLIdent(table)
137+
if err != nil {
138+
return err
139+
}
140+
escapedColumn, err := escapeMySQLIdent(column)
141+
if err != nil {
142+
return err
143+
}
144+
145+
var privilegeClauses []string
146+
for _, priv := range privileges {
147+
privilegeClauses = append(privilegeClauses, fmt.Sprintf("%s (%s)", priv, escapedColumn))
148+
}
149+
privilegesSQL := strings.Join(privilegeClauses, ", ")
150+
151+
query := fmt.Sprintf("REVOKE %s ON %s FROM '%s'", privilegesSQL, escapedTable, userRevoke)
152+
153+
_ = c.db.MustExec(query)
154+
return nil
155+
}

pkg/client/databases.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67
"strings"
78

@@ -71,3 +72,53 @@ func (c *Client) ListDatabases(ctx context.Context, pager *Pager) ([]*DbModel, s
7172

7273
return ret, nextPageToken, nil
7374
}
75+
76+
func (c *Client) GrantDatabasePrivilege(ctx context.Context, database string, user string, privilege string) error {
77+
userSplit := strings.Split(user, "@")
78+
if len(userSplit) != 2 {
79+
return fmt.Errorf("invalid user format, expected user@host")
80+
}
81+
userEsc, err := escapeMySQLUserHost(userSplit[0])
82+
if err != nil {
83+
return err
84+
}
85+
hostEsc, err := escapeMySQLUserHost(userSplit[1])
86+
if err != nil {
87+
return err
88+
}
89+
userGrant := fmt.Sprintf("'%s'@'%s'", userEsc, hostEsc)
90+
91+
escapedDB, err := escapeMySQLIdent(database)
92+
if err != nil {
93+
return err
94+
}
95+
96+
query := fmt.Sprintf("GRANT %s ON %s.* TO %s", strings.ToUpper(privilege), escapedDB, userGrant)
97+
_ = c.db.MustExec(query)
98+
return nil
99+
}
100+
101+
func (c *Client) RevokeDatabasePrivilege(ctx context.Context, database string, user string, privilege string) error {
102+
userSplit := strings.Split(user, "@")
103+
if len(userSplit) != 2 {
104+
return fmt.Errorf("invalid user format, expected user@host")
105+
}
106+
userEsc, err := escapeMySQLUserHost(userSplit[0])
107+
if err != nil {
108+
return err
109+
}
110+
hostEsc, err := escapeMySQLUserHost(userSplit[1])
111+
if err != nil {
112+
return err
113+
}
114+
userRevoke := fmt.Sprintf("'%s'@'%s'", userEsc, hostEsc)
115+
116+
escapedDB, err := escapeMySQLIdent(database)
117+
if err != nil {
118+
return err
119+
}
120+
121+
query := fmt.Sprintf("REVOKE %s ON %s.* FROM %s", strings.ToUpper(privilege), escapedDB, userRevoke)
122+
_ = c.db.MustExec(query)
123+
return nil
124+
}

pkg/client/helper.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
// Helper for identifiers (tables, columns, databases).
10+
var validIdent = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
11+
12+
func escapeMySQLIdent(ident string) (string, error) {
13+
parts := strings.Split(ident, ".")
14+
for i, part := range parts {
15+
if !validIdent.MatchString(part) {
16+
return "", fmt.Errorf("invalid identifier: %s", ident)
17+
}
18+
parts[i] = "`" + strings.ReplaceAll(part, "`", "``") + "`"
19+
}
20+
return strings.Join(parts, "."), nil
21+
}
22+
23+
// Helper for user/host.
24+
var validUserHost = regexp.MustCompile(`^[a-zA-Z0-9_%\\.\\-]+$`)
25+
26+
func escapeMySQLUserHost(ident string) (string, error) {
27+
if !validUserHost.MatchString(ident) {
28+
return "", fmt.Errorf("invalid user/host: %s", ident)
29+
}
30+
return ident, nil
31+
}

pkg/client/roles.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
func (c *Client) GrantRolePrivilege(ctx context.Context, role, user, privilege string) error {
10+
roleParts := strings.Split(role, "@")
11+
if len(roleParts) != 2 {
12+
return fmt.Errorf("invalid role format: %s", role)
13+
}
14+
15+
userParts := strings.Split(user, "@")
16+
if len(userParts) != 2 {
17+
return fmt.Errorf("invalid user format: %s", user)
18+
}
19+
20+
roleUser, err := escapeMySQLUserHost(roleParts[0])
21+
if err != nil {
22+
return err
23+
}
24+
roleHost, err := escapeMySQLUserHost(roleParts[1])
25+
if err != nil {
26+
return err
27+
}
28+
targetUser, err := escapeMySQLUserHost(userParts[0])
29+
if err != nil {
30+
return err
31+
}
32+
targetHost, err := escapeMySQLUserHost(userParts[1])
33+
if err != nil {
34+
return err
35+
}
36+
37+
var grantStmt string
38+
switch privilege {
39+
case "role_assignment":
40+
grantStmt = fmt.Sprintf("GRANT '%s'@'%s' TO '%s'@'%s'", roleUser, roleHost, targetUser, targetHost)
41+
case "role_assignment_with_grant":
42+
grantStmt = fmt.Sprintf("GRANT '%s'@'%s' TO '%s'@'%s' WITH ADMIN OPTION", roleUser, roleHost, targetUser, targetHost)
43+
case "proxy":
44+
grantStmt = fmt.Sprintf("GRANT PROXY ON '%s'@'%s' TO '%s'@'%s'", roleUser, roleHost, targetUser, targetHost)
45+
case "proxy_with_grant":
46+
grantStmt = fmt.Sprintf("GRANT PROXY ON '%s'@'%s' TO '%s'@'%s' WITH GRANT OPTION", roleUser, roleHost, targetUser, targetHost)
47+
default:
48+
return fmt.Errorf("unknown privilege: %s", privilege)
49+
}
50+
51+
_ = c.db.MustExec(grantStmt)
52+
return nil
53+
}
54+
55+
func (c *Client) RevokeRolePrivilege(ctx context.Context, role, user, privilege string) error {
56+
roleParts := strings.Split(role, "@")
57+
if len(roleParts) != 2 {
58+
return fmt.Errorf("invalid role format: %s", role)
59+
}
60+
61+
userParts := strings.Split(user, "@")
62+
if len(userParts) != 2 {
63+
return fmt.Errorf("invalid user format: %s", user)
64+
}
65+
66+
roleUser, err := escapeMySQLUserHost(roleParts[0])
67+
if err != nil {
68+
return err
69+
}
70+
roleHost, err := escapeMySQLUserHost(roleParts[1])
71+
if err != nil {
72+
return err
73+
}
74+
targetUser, err := escapeMySQLUserHost(userParts[0])
75+
if err != nil {
76+
return err
77+
}
78+
targetHost, err := escapeMySQLUserHost(userParts[1])
79+
if err != nil {
80+
return err
81+
}
82+
83+
var revokeStmt string
84+
switch privilege {
85+
case "role_assignment", "role_assignment_with_grant":
86+
revokeStmt = fmt.Sprintf("REVOKE '%s'@'%s' FROM '%s'@'%s'", roleUser, roleHost, targetUser, targetHost)
87+
case "proxy", "proxy_with_grant":
88+
revokeStmt = fmt.Sprintf("REVOKE PROXY ON '%s'@'%s' FROM '%s'@'%s'", roleUser, roleHost, targetUser, targetHost)
89+
default:
90+
return fmt.Errorf("unknown privilege: %s", privilege)
91+
}
92+
93+
_ = c.db.MustExec(revokeStmt)
94+
return nil
95+
}

0 commit comments

Comments
 (0)