@@ -24,13 +24,123 @@ func oauthCommand(appCtx *AppContext) *cli.Command {
2424 daptin oauth tokens list --provider asana.com` ,
2525 Description : "Wraps Daptin's oauth_connect and oauth_token tables without printing client secrets or tokens by default." ,
2626 Subcommands : []* cli.Command {
27+ oauthAppCommand (appCtx ),
2728 oauthConnectCommand (appCtx ),
2829 oauthLoginURLCommand (appCtx ),
2930 oauthTokensCommand (appCtx ),
3031 },
3132 }
3233}
3334
35+ func oauthAppCommand (appCtx * AppContext ) * cli.Command {
36+ return & cli.Command {
37+ Name : "app" ,
38+ Usage : "Manage Daptin OAuth provider client apps" ,
39+ Subcommands : []* cli.Command {
40+ oauthAppRegisterCommand (appCtx ),
41+ oauthAppListCommand (appCtx ),
42+ oauthAppDescribeCommand (appCtx ),
43+ oauthAppRotateSecretCommand (appCtx ),
44+ },
45+ }
46+ }
47+
48+ func oauthAppRegisterCommand (appCtx * AppContext ) * cli.Command {
49+ return & cli.Command {
50+ Name : "register" ,
51+ Usage : "Register an OAuth client app for Daptin's OAuth provider" ,
52+ ArgsUsage : "" ,
53+ UsageText : `daptin oauth app register --name "App Login" --redirect-uri https://app.example.com/auth/daptin/callback --scope openid --scope profile --scope email` ,
54+ Flags : []cli.Flag {
55+ & cli.StringFlag {Name : "name" , Usage : "OAuth app display name" },
56+ & cli.StringSliceFlag {Name : "redirect-uri" , Usage : "Allowed redirect URI; repeat for multiple values" },
57+ & cli.StringSliceFlag {Name : "scope" , Usage : "Allowed scope; repeat or pass comma-separated values" },
58+ & cli.StringSliceFlag {Name : "grant" , Usage : "Allowed grant; repeat or pass comma-separated values" },
59+ & cli.BoolFlag {Name : "confidential" , Usage : "Register a confidential client (default)" },
60+ & cli.BoolFlag {Name : "public" , Usage : "Register a public client with no client secret" },
61+ },
62+ Action : func (c * cli.Context ) error {
63+ name := strings .TrimSpace (c .String ("name" ))
64+ if name == "" {
65+ return fmt .Errorf ("--name is required" )
66+ }
67+ if c .Bool ("confidential" ) && c .Bool ("public" ) {
68+ return fmt .Errorf ("--confidential and --public are mutually exclusive" )
69+ }
70+ redirectURIs := c .StringSlice ("redirect-uri" )
71+ if len (redirectURIs ) == 0 {
72+ return fmt .Errorf ("--redirect-uri is required" )
73+ }
74+
75+ attrs := oauthAppRegisterAttrs (name , redirectURIs , c .StringSlice ("scope" ), c .StringSlice ("grant" ), ! c .Bool ("public" ))
76+ responses , err := appCtx .Client .Execute ("register_client" , "oauth_app" , daptinClient .JsonApiObject (attrs ))
77+ if err != nil {
78+ return err
79+ }
80+ return renderOAuthAppActionResponse (appCtx , responses , false )
81+ },
82+ }
83+ }
84+
85+ func oauthAppListCommand (appCtx * AppContext ) * cli.Command {
86+ return & cli.Command {
87+ Name : "list" ,
88+ Usage : "List OAuth provider client apps without secrets" ,
89+ Action : func (c * cli.Context ) error {
90+ result , err := appCtx .Client .FindAll ("oauth_app" , daptinClient.DaptinQueryParameters {"page[size]" : 100 })
91+ if err != nil {
92+ return err
93+ }
94+ rows := client .MapArray (result , "attributes" )
95+ rows = render .FilterColumns (rows , []string {"name" , "client_id" , "redirect_uris" , "scopes" , "grants" , "is_confidential" , "is_enabled" , "reference_id" })
96+ if appCtx .Quiet {
97+ return printRefs (rows )
98+ }
99+ return appCtx .Renderer .RenderArray (rows )
100+ },
101+ }
102+ }
103+
104+ func oauthAppDescribeCommand (appCtx * AppContext ) * cli.Command {
105+ return & cli.Command {
106+ Name : "describe" ,
107+ Usage : "Describe one OAuth provider client app without secrets" ,
108+ ArgsUsage : "<name-client-id-or-reference-id>" ,
109+ Action : func (c * cli.Context ) error {
110+ app , err := oauthAppByNameClientOrRef (appCtx , c .Args ().Get (0 ))
111+ if err != nil {
112+ return err
113+ }
114+ return renderSingleAPIObject (appCtx , app )
115+ },
116+ }
117+ }
118+
119+ func oauthAppRotateSecretCommand (appCtx * AppContext ) * cli.Command {
120+ return & cli.Command {
121+ Name : "rotate-secret" ,
122+ Usage : "Rotate a confidential OAuth app client secret" ,
123+ ArgsUsage : "<name-client-id-or-reference-id>" ,
124+ Action : func (c * cli.Context ) error {
125+ app , err := oauthAppByNameClientOrRef (appCtx , c .Args ().Get (0 ))
126+ if err != nil {
127+ return err
128+ }
129+ ref := refID (app )
130+ if ref == "" {
131+ return fmt .Errorf ("oauth_app %q has no reference_id" , c .Args ().Get (0 ))
132+ }
133+ responses , err := appCtx .Client .Execute ("rotate_client_secret" , "oauth_app" , daptinClient.JsonApiObject {
134+ "oauth_app_id" : ref ,
135+ })
136+ if err != nil {
137+ return err
138+ }
139+ return renderOAuthAppActionResponse (appCtx , responses , false )
140+ },
141+ }
142+ }
143+
34144func oauthConnectCommand (appCtx * AppContext ) * cli.Command {
35145 return & cli.Command {
36146 Name : "connect" ,
@@ -296,6 +406,84 @@ func redirectURLFromResponses(responses []daptinClient.DaptinActionResponse) str
296406 return ""
297407}
298408
409+ func oauthAppRegisterAttrs (name string , redirectURIs , scopes , grants []string , confidential bool ) map [string ]interface {} {
410+ attrs := map [string ]interface {}{
411+ "name" : name ,
412+ "redirect_uris" : oauthListString (redirectURIs , "" ),
413+ "scopes" : oauthListString (scopes , "openid profile email" ),
414+ "grants" : oauthListString (grants , "authorization_code refresh_token" ),
415+ "is_confidential" : confidential ,
416+ }
417+ return attrs
418+ }
419+
420+ func oauthListString (values []string , defaultValue string ) string {
421+ parts := make ([]string , 0 , len (values ))
422+ for _ , value := range values {
423+ value = strings .ReplaceAll (value , "," , " " )
424+ for _ , part := range strings .Fields (value ) {
425+ if part != "" {
426+ parts = append (parts , part )
427+ }
428+ }
429+ }
430+ if len (parts ) == 0 {
431+ return defaultValue
432+ }
433+ return strings .Join (parts , " " )
434+ }
435+
436+ func oauthAppByNameClientOrRef (appCtx * AppContext , nameClientOrRef string ) (map [string ]interface {}, error ) {
437+ if nameClientOrRef == "" {
438+ return nil , fmt .Errorf ("oauth app name, client_id, or reference_id required" )
439+ }
440+ if strings .Contains (nameClientOrRef , "-" ) {
441+ row , err := appCtx .Client .FindOne ("oauth_app" , nameClientOrRef , nil )
442+ if err == nil {
443+ return row , nil
444+ }
445+ }
446+ if row , err := findOneByField (appCtx , "oauth_app" , "client_id" , nameClientOrRef ); err == nil {
447+ return row , nil
448+ }
449+ return findOneByName (appCtx , "oauth_app" , nameClientOrRef )
450+ }
451+
452+ func findOneByField (appCtx * AppContext , entityName , fieldName , value string ) (map [string ]interface {}, error ) {
453+ clauses , err := ParseFilter (fieldName + "=" + value )
454+ if err != nil {
455+ return nil , err
456+ }
457+ result , err := appCtx .Client .FindAll (entityName , daptinClient.DaptinQueryParameters {
458+ "page[size]" : 1 ,
459+ "query" : FilterToJSON (clauses ),
460+ })
461+ if err != nil {
462+ return nil , err
463+ }
464+ if len (result ) == 0 {
465+ return nil , fmt .Errorf ("%s with %s %q not found" , entityName , fieldName , value )
466+ }
467+ return result [0 ], nil
468+ }
469+
470+ func renderOAuthAppActionResponse (appCtx * AppContext , responses []daptinClient.DaptinActionResponse , redact bool ) error {
471+ for _ , response := range responses {
472+ if response .ResponseType != "oauth_app" {
473+ continue
474+ }
475+ attrs := response .Attributes
476+ if redact {
477+ attrs = redactSecretColumns (attrs )
478+ }
479+ if appCtx .Quiet {
480+ return printRef (attrs )
481+ }
482+ return appCtx .Renderer .RenderObject (attrs )
483+ }
484+ return applyEffects (ProcessResponses (responses ), appCtx )
485+ }
486+
299487func openBrowser (url string ) error {
300488 var command string
301489 var args []string
0 commit comments