How gog Implements Least-Privilege Authentication with `--readonly` and `--drive-scope` Flags

The gog CLI constructs minimal OAuth scope sets by using --readonly to force read-only variants across all selected services and --drive-scope to granularly control Google Drive permissions, with built-in validation that prevents conflicting write-capable and read-only combinations.

The steipete/gogcli repository implements least-privilege authentication by dynamically building OAuth scope lists tailored to the exact services a user requests. Rather than requesting broad, all-encompassing permissions, the tool uses the --readonly and --drive-scope flags to narrow access to precisely what operations require. This approach minimizes security exposure while maintaining full compatibility with Google Workspace APIs.

Flag Definition and Validation

The command-line interface defines the least-privilege flags in internal/cmd/auth.go. The AuthAddCmd struct captures user preferences through two key fields:

type AuthAddCmd struct {
    // ... other fields ...
    Readonly   bool   `name:"readonly" help:"Use read-only scopes where available (still includes OIDC identity scopes)"`
    DriveScope string `name:"drive-scope" help:"Drive scope mode: full|readonly|file" enum:"full,readonly,file" default:"full"`
}

Before generating tokens, the Run method validates flag combinations to prevent logical conflicts. Because --drive-scope=file grants write capability to files created by the application, it cannot coexist with --readonly:

if c.Readonly && c.DriveScope == strFile {
    return usage("cannot combine --readonly with --drive-scope=file (file is write-capable)")
}

This validation ensures users cannot accidentally request a read-only token that simultaneously claims write access to Drive files.

Building Minimal Scope Lists

The core scope-generation logic resides in internal/googleauth/service.go. The ScopesForManageWithOptions function accepts a list of requested services and a ScopeOptions struct containing the flag values:

scopes, err := googleauth.ScopesForManageWithOptions(services, googleauth.ScopeOptions{
    Readonly:   c.Readonly,
    DriveScope: googleauth.DriveScopeMode(c.DriveScope),
})

Inside scopesForServiceWithOptions, the code determines the appropriate Drive scope through a closure that evaluates flags in priority order:

driveScopeValue := func() string {
    if opts.Readonly {
        return "https://www.googleapis.com/auth/drive.readonly"
    }
    switch opts.DriveScope {
    case DriveScopeFile:
        return "https://www.googleapis.com/auth/drive.file"
    case DriveScopeReadonly:
        return "https://www.googleapis.com/auth/drive.readonly"
    default:
        return "https://www.googleapis.com/auth/drive"
    }
}

When opts.Readonly is true, this helper forces the Drive scope to drive.readonly regardless of the --drive-scope setting. For other services like Gmail, Calendar, and Contacts, the code checks opts.Readonly to swap standard scopes for their read-only variants (e.g., gmail.readonly instead of gmail.modify).

Finally, ScopesForManageWithOptions appends the essential OpenID Connect identity scopes to every token request:

return mergeScopes(scopes, []string{scopeOpenID, scopeEmail, scopeUserinfoEmail}), nil

This ensures authentication works for user identity verification while keeping service access minimal.

Testing Least-Privilege Logic

The repository validates these behaviors in internal/googleauth/service_test.go. Key test cases include:

  • TestScopesForManageWithOptions_Readonly: Confirms that every service returns read-only scope variants when the Readonly option is true.
  • TestScopesForManageWithOptions_DriveScopeFile: Verifies that drive.file is selected only when explicitly requested and --readonly is not set.
  • TestScopesForManageWithOptions_InvalidDriveScope: Ensures the function returns an error for unrecognized DriveScope values.

These tests guarantee that flag combinations produce predictable, minimal permission sets.

Practical Usage Examples

The following commands demonstrate how --readonly and --drive-scope generate different OAuth scope combinations:


# Read-only access to Drive and Gmail

gog auth add [email protected] --services drive,gmail --readonly

# Scopes: openid, email, userinfo.email, drive.readonly, gmail.readonly

# Write access only to files created by the app, plus full Docs access

gog auth add [email protected] --services drive,docs --drive-scope=file

# Scopes: openid, email, userinfo.email, drive.file, documents

# This combination is rejected:

gog auth add [email protected] --services drive --readonly --drive-scope=file

# Error: cannot combine --readonly with --drive-scope=file (file is write-capable)

Summary

  • --readonly forces read-only OAuth scopes across all selected services while preserving essential identity scopes (openid, email, userinfo.email).
  • --drive-scope provides granular control over Google Drive permissions, supporting full, readonly, and file modes.
  • Validation logic in internal/cmd/auth.go prevents conflicting flag combinations (e.g., --readonly with drive.file) before token generation begins.
  • Scope construction in internal/googleauth/service.go dynamically builds minimal permission sets based on the validated flags, ensuring least-privilege access to Google APIs.

Frequently Asked Questions

What happens if I use --readonly with --drive-scope=full?

When --readonly is set, it takes precedence over the --drive-scope setting for Drive permissions. According to the logic in internal/googleauth/service.go, the driveScopeValue helper checks opts.Readonly first and returns https://www.googleapis.com/auth/drive.readonly regardless of the DriveScope value. The --drive-scope flag still affects validation logic, but the resulting token will only have read-only Drive access.

Why can't I combine --readonly with --drive-scope=file?

The combination is blocked because drive.file grants write capability to files created or opened by the application, which directly contradicts the global read-only request. As implemented in internal/cmd/auth.go lines 107-109, the CLI aborts with the error message "cannot combine --readonly with --drive-scope=file (file is write-capable)" before initiating the OAuth flow. This validation ensures logical consistency in permission requests.

Does --readonly affect all Google services or just Drive?

The --readonly flag affects all selected services that offer read-only OAuth scopes. In internal/googleauth/service.go, the scopesForServiceWithOptions function checks opts.Readonly for every service type. For example, Gmail switches from gmail.modify to gmail.readonly, Calendar switches to calendar.readonly, and Docs switches to documents.readonly. The flag is applied globally across the entire permission set.

What are the default scopes if I don't specify any flags?

If you run gog auth add without --readonly or --drive-scope, the tool defaults to requesting full read/write access for Drive and standard access levels for other services. Specifically, --drive-scope defaults to full (granting https://www.googleapis.com/auth/drive), and Readonly defaults to false. Every authentication request automatically includes the identity scopes openid, email, and userinfo.email regardless of other flags.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →