Skip to content

Commit 277b8b8

Browse files
authored
support external JWKS commands (#993)
2 parents 1c7b39a + 5f05d8d commit 277b8b8

File tree

8 files changed

+371
-15
lines changed

8 files changed

+371
-15
lines changed

internal/cmd/db_generatetoken.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func init() {
1919
flags.AddExpiration(dbGenerateTokenCmd)
2020
flags.AddReadOnly(dbGenerateTokenCmd)
2121
flags.AddAttachClaims(dbGenerateTokenCmd)
22+
flags.AddFineGrainedPermissions(dbGenerateTokenCmd)
2223
dbGenerateTokenCmd.Flags().BoolVar(&groupTokenFlag, "group", false, "create a token that is valid for all databases in the group")
2324
}
2425

@@ -55,7 +56,11 @@ var dbGenerateTokenCmd = &cobra.Command{
5556
ReadAttach: turso.Entities{DBNames: flags.AttachClaims()},
5657
}
5758
}
58-
token, err := getToken(client, database, expiration, flags.ReadOnly(), groupTokenFlag, claim)
59+
permissions, err := flags.FineGrainedPermissionsFlags()
60+
if err != nil {
61+
return err
62+
}
63+
token, err := getToken(client, database, expiration, flags.ReadOnly(), groupTokenFlag, claim, permissions)
5964
if err != nil {
6065
return errors.New("your database does not support token generation")
6166
}
@@ -64,12 +69,19 @@ var dbGenerateTokenCmd = &cobra.Command{
6469
},
6570
}
6671

67-
func getToken(client *turso.Client, database turso.Database, expiration string, readOnly, group bool, claim *turso.PermissionsClaim) (string, error) {
72+
func getToken(
73+
client *turso.Client,
74+
database turso.Database,
75+
expiration string,
76+
readOnly, group bool,
77+
claim *turso.PermissionsClaim,
78+
fineGrainedPermissions []flags.FineGrainedPermissions,
79+
) (string, error) {
6880
if !group {
69-
return client.Databases.Token(database.Name, expiration, readOnly, claim)
81+
return client.Databases.Token(database.Name, expiration, readOnly, claim, fineGrainedPermissions)
7082
}
7183
if group && database.Group == "" {
7284
return "", errors.New("--group flag can only be set with group databases")
7385
}
74-
return client.Groups.Token(database.Group, expiration, readOnly, claim)
86+
return client.Groups.Token(database.Group, expiration, readOnly, claim, fineGrainedPermissions)
7587
}

internal/cmd/db_shell.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,14 @@ func tokenFromDb(db *turso.Database, client *turso.Client, claim *turso.Permissi
337337
}
338338
// skip cache and always use token from server when claims are attached
339339
if claim != nil {
340-
return client.Databases.Token(db.Name, "2d", false, claim)
340+
return client.Databases.Token(db.Name, "2d", false, claim, nil)
341341
}
342342

343343
if token := dbTokenCache(db.ID); token != "" {
344344
return token, nil
345345
}
346346

347-
token, err := client.Databases.Token(db.Name, "2d", false, nil)
347+
token, err := client.Databases.Token(db.Name, "2d", false, nil, nil)
348348
if err != nil {
349349
return "", err
350350
}

internal/cmd/group_tokens.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func init() {
8888
flags.AddExpiration(groupCreateTokenCmd)
8989
flags.AddReadOnly(groupCreateTokenCmd)
9090
flags.AddAttachClaims(groupCreateTokenCmd)
91+
flags.AddFineGrainedPermissions(groupCreateTokenCmd)
9192
}
9293

9394
var groupCreateTokenCmd = &cobra.Command{
@@ -122,7 +123,11 @@ var groupCreateTokenCmd = &cobra.Command{
122123
ReadAttach: turso.Entities{DBNames: flags.AttachClaims()},
123124
}
124125
}
125-
token, err := client.Groups.Token(group.Name, expiration, flags.ReadOnly(), claim)
126+
permission, err := flags.FineGrainedPermissionsFlags()
127+
if err != nil {
128+
return err
129+
}
130+
token, err := client.Groups.Token(group.Name, expiration, flags.ReadOnly(), claim, permission)
126131
if err != nil {
127132
return fmt.Errorf("error creating token: %w", err)
128133
}

internal/cmd/org.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import (
88

99
"github.com/spf13/cobra"
1010
"github.com/tursodatabase/turso-cli/internal"
11+
"github.com/tursodatabase/turso-cli/internal/flags"
1112
"github.com/tursodatabase/turso-cli/internal/prompt"
1213
"github.com/tursodatabase/turso-cli/internal/settings"
1314
"github.com/tursodatabase/turso-cli/internal/turso"
1415
)
1516

1617
var adminFlag bool
18+
var jwksRegion string
19+
var jwksDatabase string
20+
var jwksGroup string
21+
var jwksScope string
1722

1823
func init() {
1924
rootCmd.AddCommand(orgCmd)
@@ -38,6 +43,21 @@ func init() {
3843
orgCmd.AddCommand(orgBillingCmd)
3944
membersAddCmd.Flags().BoolVarP(&adminFlag, "admin", "a", false, "Add the user as an admin")
4045
membersInviteCmd.Flags().BoolVarP(&adminFlag, "admin", "a", false, "Invite the user as an admin")
46+
47+
orgCmd.AddCommand(jwksCmd)
48+
jwksCmd.AddCommand(jwksList)
49+
jwksCmd.AddCommand(jwksRemove)
50+
jwksCmd.AddCommand(jwksSave)
51+
jwksCmd.AddCommand(jwksTemplate)
52+
53+
jwksSave.Flags().StringVarP(&jwksRegion, "region", "r", "", "region")
54+
jwksSave.Flags().MarkHidden("region")
55+
jwksRemove.Flags().StringVarP(&jwksRegion, "region", "r", "", "region")
56+
jwksRemove.Flags().MarkHidden("region")
57+
jwksTemplate.Flags().StringVarP(&jwksDatabase, "database", "d", "", "database")
58+
jwksTemplate.Flags().StringVarP(&jwksGroup, "group", "g", "", "group")
59+
jwksTemplate.Flags().StringVarP(&jwksScope, "scope", "s", "full-access", "claims scope (full-access or read-only)")
60+
flags.AddFineGrainedPermissions(jwksTemplate)
4161
}
4262

4363
func switchToOrg(client *turso.Client, slug string, showHowToGoBack bool) error {
@@ -622,6 +642,165 @@ func BillingPortal() error {
622642
return billingPortal(org)
623643
}
624644

645+
func currentOrg(client *turso.Client) (turso.Organization, error) {
646+
orgs, err := client.Organizations.List()
647+
if err != nil {
648+
return turso.Organization{}, err
649+
}
650+
651+
settingsObj, err := settings.ReadSettings()
652+
if err != nil {
653+
return turso.Organization{}, err
654+
}
655+
656+
current := settingsObj.Organization()
657+
658+
for _, org := range orgs {
659+
if isCurrentOrg(org, current) {
660+
return org, nil
661+
}
662+
}
663+
return turso.Organization{}, fmt.Errorf("current organization is not set")
664+
}
665+
666+
var jwksCmd = &cobra.Command{
667+
Use: "jwks",
668+
Short: "[BETA] Manage your organization external JWKS sources",
669+
}
670+
671+
var jwksList = &cobra.Command{
672+
Use: "list",
673+
Short: "List saved external JWKS sources",
674+
ValidArgsFunction: noFilesArg,
675+
RunE: func(cmd *cobra.Command, args []string) error {
676+
cmd.SilenceUsage = true
677+
678+
client, err := authedTursoClient()
679+
if err != nil {
680+
return err
681+
}
682+
683+
org, err := currentOrg(client)
684+
if err != nil {
685+
return err
686+
}
687+
688+
jwksList, err := client.Organizations.ListJwks(org.Slug)
689+
if err != nil {
690+
return err
691+
}
692+
table := make([][]string, 0)
693+
for _, jwks := range jwksList {
694+
table = append(table, []string{jwks.JwksName, jwks.JwksUrl})
695+
}
696+
printTable([]string{"name", "url"}, table)
697+
return nil
698+
},
699+
}
700+
701+
var jwksTemplate = &cobra.Command{
702+
Use: "template",
703+
Short: "Generate JWT claims template for external auth provider",
704+
ValidArgsFunction: noFilesArg,
705+
RunE: func(cmd *cobra.Command, args []string) error {
706+
cmd.SilenceUsage = true
707+
708+
client, err := authedTursoClient()
709+
if err != nil {
710+
return err
711+
}
712+
713+
org, err := currentOrg(client)
714+
if err != nil {
715+
return err
716+
}
717+
718+
var database *string
719+
var group *string
720+
721+
if jwksDatabase != "" {
722+
database = &jwksDatabase
723+
}
724+
if jwksGroup != "" {
725+
group = &jwksGroup
726+
}
727+
permissions, err := flags.FineGrainedPermissionsFlags()
728+
if err != nil {
729+
return err
730+
}
731+
params := turso.OrgJwksTemplateParams{
732+
Database: database,
733+
Group: group,
734+
Scope: jwksScope,
735+
Permissions: permissions,
736+
}
737+
template, err := client.Organizations.JwksTemplate(org.Slug, params)
738+
if err != nil {
739+
return err
740+
}
741+
fmt.Printf("%v\n", internal.Emph(template))
742+
return nil
743+
},
744+
}
745+
746+
var jwksSave = &cobra.Command{
747+
Use: "save <name> <url>",
748+
Short: "Save external JWKS source with given name and URL (e.g. save clerk https://.../.well-known/jwks.json)",
749+
Args: cobra.ExactArgs(2),
750+
ValidArgsFunction: noFilesArg,
751+
RunE: func(cmd *cobra.Command, args []string) error {
752+
cmd.SilenceUsage = true
753+
754+
name, url := args[0], args[1]
755+
756+
client, err := authedTursoClient()
757+
if err != nil {
758+
return err
759+
}
760+
761+
org, err := currentOrg(client)
762+
if err != nil {
763+
return err
764+
}
765+
766+
err = client.Organizations.SaveJwks(org.Slug, name, url, jwksRegion)
767+
if err != nil {
768+
return err
769+
}
770+
fmt.Printf("JWKS %v saved for organization %s.\n", internal.Emph(name), internal.Emph(org.Slug))
771+
return nil
772+
},
773+
}
774+
775+
var jwksRemove = &cobra.Command{
776+
Use: "remove <name>",
777+
Short: "Remove external JWKS source with given name",
778+
Args: cobra.ExactArgs(1),
779+
ValidArgsFunction: noFilesArg,
780+
RunE: func(cmd *cobra.Command, args []string) error {
781+
cmd.SilenceUsage = true
782+
783+
name := args[0]
784+
785+
client, err := authedTursoClient()
786+
if err != nil {
787+
return err
788+
}
789+
790+
org, err := currentOrg(client)
791+
if err != nil {
792+
return err
793+
}
794+
795+
err = client.Organizations.RemoveJwks(org.Slug, name, jwksRegion)
796+
if err != nil {
797+
return err
798+
}
799+
fmt.Printf("JWKS %v removed from organization %s.\n", internal.Emph(name), internal.Emph(org.Slug))
800+
return nil
801+
},
802+
}
803+
625804
func listOrganizations(client *turso.Client, fresh ...bool) ([]turso.Organization, error) {
626805
skipCache := len(fresh) > 0 && fresh[0]
627806
if cache := getOrgsCache(); !skipCache && cache != nil {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package flags
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
type FineGrainedPermissions struct {
11+
TableNames []string `json:"t"`
12+
AllowedOperations []string `json:"a"`
13+
}
14+
15+
var fineGrainedPermissions []string
16+
17+
func AddFineGrainedPermissions(cmd *cobra.Command) {
18+
cmd.Flags().StringArrayVarP(&fineGrainedPermissions, "permissions", "p", nil, "fine-grained permissions in format <table-name|all>:<action1>,...\n(e.g: -p all:data_read -p comments:data_insert)")
19+
}
20+
21+
func FineGrainedPermissionsFlags() ([]FineGrainedPermissions, error) {
22+
permissions := make([]FineGrainedPermissions, 0)
23+
for _, permission := range fineGrainedPermissions {
24+
tokens := strings.SplitN(permission, ":", 2)
25+
if len(tokens) != 2 {
26+
return nil, fmt.Errorf("invalid permission format: '%v'", permission)
27+
}
28+
var tableNames []string
29+
if tokens[0] != "all" {
30+
tableNames = append(tableNames, tokens[0])
31+
}
32+
permissions = append(permissions, FineGrainedPermissions{
33+
TableNames: tableNames,
34+
AllowedOperations: strings.Split(tokens[1], ","),
35+
})
36+
}
37+
return permissions, nil
38+
}

internal/turso/databases.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/tursodatabase/turso-cli/internal"
14+
"github.com/tursodatabase/turso-cli/internal/flags"
1415
"github.com/tursodatabase/turso-cli/internal/prompt"
1516
)
1617

@@ -218,7 +219,7 @@ func (d *DatabasesClient) Create(name, location, image, extensions, group string
218219
// turso-server will perform validations on the file and 'activate' the db if everything is ok.
219220
func (d *DatabasesClient) UploadDatabaseAWS(resp *CreateDatabaseResponse, group string, uploadFilepath string, spinner *prompt.SpinnerT) (*CreateDatabaseResponse, error) {
220221
// Create a short-lived DB token for the newly created database to facilitate the upload
221-
token, err := d.Token(resp.Database.Name, "1h", false, nil)
222+
token, err := d.Token(resp.Database.Name, "1h", false, nil, nil)
222223
if err != nil {
223224
return nil, fmt.Errorf("could not create database token: %w", err)
224225
}
@@ -272,7 +273,7 @@ func (d *DatabasesClient) Export(dbName, dbUrl, outputFile string, withMetadata
272273
return fmt.Errorf("file %s already exists, use `--overwrite` flag to overwrite it", outputFile)
273274
}
274275
}
275-
token, err := d.Token(dbName, "1h", false, nil)
276+
token, err := d.Token(dbName, "1h", false, nil, nil)
276277
if err != nil {
277278
return fmt.Errorf("could not create database token: %w", err)
278279
}
@@ -336,17 +337,24 @@ func (d *DatabasesClient) UploadDump(dbFile *os.File) (string, error) {
336337
}
337338

338339
type DatabaseTokenRequest struct {
339-
Permissions *PermissionsClaim `json:"permissions,omitempty"`
340+
Permissions *PermissionsClaim `json:"permissions,omitempty"`
341+
FineGrainedPermissions []flags.FineGrainedPermissions `json:"fine_grained_permissions,omitempty"`
340342
}
341343

342-
func (d *DatabasesClient) Token(database string, expiration string, readOnly bool, permissions *PermissionsClaim) (string, error) {
344+
func (d *DatabasesClient) Token(
345+
database string,
346+
expiration string,
347+
readOnly bool,
348+
permissions *PermissionsClaim,
349+
fineGrainedPermissions []flags.FineGrainedPermissions,
350+
) (string, error) {
343351
authorization := ""
344352
if readOnly {
345353
authorization = "&authorization=read-only"
346354
}
347355
url := d.URL(fmt.Sprintf("/%s/auth/tokens?expiration=%s%s", database, expiration, authorization))
348356

349-
req := DatabaseTokenRequest{permissions}
357+
req := DatabaseTokenRequest{Permissions: permissions, FineGrainedPermissions: fineGrainedPermissions}
350358
body, err := marshal(req)
351359
if err != nil {
352360
return "", fmt.Errorf("could not serialize request body: %w", err)

0 commit comments

Comments
 (0)