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

> Learn how gog implements least-privilege authentication using --readonly and --drive-scope flags for secure, granular access control. Minimize permissions effectively.

- Repository: [Peter Steinberger/gogcli](https://github.com/steipete/gogcli)
- Tags: how-to-guide
- Published: 2026-02-16

---

**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`](https://github.com/steipete/gogcli/blob/main/internal/cmd/auth.go)**. The `AuthAddCmd` struct captures user preferences through two key fields:

```go
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`:

```go
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`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/service.go)**. The `ScopesForManageWithOptions` function accepts a list of requested services and a `ScopeOptions` struct containing the flag values:

```go
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:

```go
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:

```go
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`](https://github.com/steipete/gogcli/blob/main/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:

```bash

# Read-only access to Drive and Gmail

gog auth add alice@example.com --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 bob@example.com --services drive,docs --drive-scope=file

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

# This combination is rejected:

gog auth add eve@example.com --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`](https://github.com/steipete/gogcli/blob/main/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`](https://github.com/steipete/gogcli/blob/main/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`](https://github.com/steipete/gogcli/blob/main/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`](https://github.com/steipete/gogcli/blob/main/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`](https://github.com/steipete/gogcli/blob/main/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.