Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,32 @@ baton resources
# Data Model

`baton-google-workspace` will pull down information about the following Google Workspace resources:

- Groups
- Users
- Roles
- Tokens

## Scope Permissions

In Admin Console → Security → API controls → Manage domain-wide delegation, authorize the service account client ID with
those scopes.

- https://www.googleapis.com/auth/admin.directory.rolemanagement
- https://www.googleapis.com/auth/admin.directory.user.alias.readonly
- https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
- https://www.googleapis.com/auth/admin.directory.group.member.readonly
- https://www.googleapis.com/auth/admin.directory.group.readonly
- https://www.googleapis.com/auth/admin.directory.user.readonly
- https://www.googleapis.com/auth/admin.directory.domain.readonly
- https://www.googleapis.com/auth/admin.reports.audit.readonly
- https://www.googleapis.com/auth/admin.directory.user.security

# Contributing, Support and Issues

We started Baton because we were tired of taking screenshots and manually building spreadsheets. We welcome contributions, and ideas, no matter how small -- our goal is to make identity and permissions sprawl less painful for everyone. If you have questions, problems, or ideas: Please open a Github Issue!
We started Baton because we were tired of taking screenshots and manually building spreadsheets. We welcome
contributions, and ideas, no matter how small -- our goal is to make identity and permissions sprawl less painful for
everyone. If you have questions, problems, or ideas: Please open a Github Issue!

See [CONTRIBUTING.md](https://github.com/ConductorOne/baton/blob/main/CONTRIBUTING.md) for more details.

Expand Down
6 changes: 6 additions & 0 deletions cmd/baton-google-workspace/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,19 @@ var (
field.WithDescription("JSON credentials for the Google Workspace account. Mutually exclusive with file path"),
)

SyncTokensField = field.BoolField(
"sync-tokens",
field.WithDescription("Sync third party tokens for the Google Workspace account."),
)

// Collection of all configuration fields.
ConfigurationFields = []field.SchemaField{
CustomerIDField,
DomainField,
AdministratorEmailField,
CredentialsJSONFilePathField,
CredentialsJSONField,
SyncTokensField,
}

// Configuration combines fields into a single configuration object.
Expand Down
2 changes: 2 additions & 0 deletions cmd/baton-google-workspace/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e
administratorEmail := v.GetString(AdministratorEmailField.FieldName)
credentialsJSONFilePath := v.GetString(CredentialsJSONFilePathField.FieldName)
credentialsJSON := v.GetString(CredentialsJSONField.FieldName)
syncTokens := v.GetBool(SyncTokensField.FieldName)

var jsonCredentials []byte

Expand Down Expand Up @@ -83,6 +84,7 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e
AdministratorEmail: administratorEmail,
Domain: domain,
Credentials: jsonCredentials,
SyncTokens: syncTokens,
}

// Create the Google Workspace connector
Expand Down
20 changes: 19 additions & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,22 @@ var (
},
},
}

resourceTypeUserToken = &v2.ResourceType{
Id: "user_token",
DisplayName: "User Tokens",
Traits: []v2.ResourceType_Trait{
v2.ResourceType_TRAIT_APP,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be secret trait instead?

},
}
)

type Config struct {
CustomerID string
AdministratorEmail string
Domain string
Credentials []byte
SyncTokens bool
}

type GoogleWorkspace struct {
Expand All @@ -102,6 +111,7 @@ type GoogleWorkspace struct {
primaryDomain string
domainsCache []string
reportService *reportsAdmin.Service
syncTokens bool
}

type newService[T any] func(ctx context.Context, opts ...option.ClientOption) (*T, error)
Expand Down Expand Up @@ -169,6 +179,7 @@ func New(ctx context.Context, config Config) (*GoogleWorkspace, error) {
credentials: config.Credentials,
serviceCache: map[string]any{},
domain: config.Domain,
syncTokens: config.SyncTokens,
}
return rv, nil
}
Expand Down Expand Up @@ -278,7 +289,7 @@ func (c *GoogleWorkspace) ResourceSyncers(ctx context.Context) []connectorbuilde

userService, err := c.getDirectoryService(ctx, directoryAdmin.AdminDirectoryUserReadonlyScope)
if err == nil {
rs = append(rs, userBuilder(userService, c.customerID, c.domain))
rs = append(rs, userBuilder(userService, c.customerID, c.domain, c.syncTokens))
}

// We don't care about the error here, as we handle the case where the service is nil in the syncer
Expand All @@ -296,6 +307,13 @@ func (c *GoogleWorkspace) ResourceSyncers(ctx context.Context) []connectorbuilde
))
}
}

if c.syncTokens {
if userTokenService, err := c.getDirectoryService(ctx, directoryAdmin.AdminDirectoryUserSecurityScope); err == nil {
rs = append(rs, newUserTokenResource(userTokenService))
}
}

return rs
}

Expand Down
29 changes: 23 additions & 6 deletions pkg/connector/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type userResourceType struct {
userService *admin.Service
customerId string
domain string
syncTokens bool
}

func (o *userResourceType) ResourceType(_ context.Context) *v2.ResourceType {
Expand Down Expand Up @@ -112,12 +113,13 @@ func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ *paginati
return nil, "", nil, nil
}

func userBuilder(userService *admin.Service, customerId string, domain string) *userResourceType {
func userBuilder(userService *admin.Service, customerId string, domain string, syncTokens bool) *userResourceType {
return &userResourceType{
resourceType: resourceTypeUser,
userService: userService,
customerId: customerId,
domain: domain,
syncTokens: syncTokens,
}
}

Expand Down Expand Up @@ -364,16 +366,31 @@ func (o *userResourceType) userResource(ctx context.Context, user *admin.User) (
sdkResource.WithUserLogin(user.PrimaryEmail, additionalLogins.ToSlice()...),
)

userResource, err := sdkResource.NewUserResource(
user.Name.FullName,
resourceTypeUser,
user.Id,
traitOpts,
rsOption := []sdkResource.ResourceOption{
sdkResource.WithAnnotation(
&v2.V1Identifier{
Id: user.Id,
},
),
}

if o.syncTokens {
rsOption = append(
rsOption,
sdkResource.WithAnnotation(
&v2.ChildResourceType{
ResourceTypeId: resourceTypeUserToken.Id,
},
),
)
}

userResource, err := sdkResource.NewUserResource(
user.Name.FullName,
resourceTypeUser,
user.Id,
traitOpts,
rsOption...,
)
return userResource, err
}
Expand Down
137 changes: 137 additions & 0 deletions pkg/connector/user_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package connector

import (
"context"
"fmt"
"strings"

"github.com/conductorone/baton-sdk/pkg/types/entitlement"
"github.com/conductorone/baton-sdk/pkg/types/grant"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/pagination"
sdkResource "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
admin "google.golang.org/api/admin/directory/v1"
)

type userTokenResource struct {
userService *admin.Service
}

func newUserTokenResource(userService *admin.Service) *userTokenResource {
return &userTokenResource{userService: userService}
}

func (u *userTokenResource) ResourceType(ctx context.Context) *v2.ResourceType {
return resourceTypeUserToken
}

func (u *userTokenResource) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)

if parentResourceID == nil {
l.Info("Skipping user token resource type list, only supported as a child resource type")
return nil, "", nil, nil
}

if parentResourceID.ResourceType != resourceTypeUser.Id {
return nil, "", nil, fmt.Errorf("invalid resource type: %s", parentResourceID.ResourceType)
}

userKey := parentResourceID.Resource

doResponse, err := u.userService.Tokens.List(userKey).Context(ctx).Do()
if err != nil {
return nil, "", nil, err
}

rv := make([]*v2.Resource, 0, len(doResponse.Items))
for _, token := range doResponse.Items {
profile := map[string]any{
"client_id": token.ClientId,
"display_text": token.DisplayText,
"scopes": strings.Join(token.Scopes, " "),
"user_key": token.UserKey,
}

opts := []sdkResource.AppTraitOption{
sdkResource.WithAppProfile(profile),
}

rs, err := sdkResource.NewAppResource(
token.DisplayText,
resourceTypeUserToken,
fmt.Sprintf("%s/%s", token.UserKey, token.ClientId),
opts,
sdkResource.WithParentResourceID(parentResourceID),
)

if err != nil {
l.Error("Failed to create resource for user token", zap.Error(err))
continue
}

rv = append(rv, rs)
}

return rv, "", nil, nil
}

func (u *userTokenResource) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) {
return []*v2.Entitlement{
entitlement.NewAssignmentEntitlement(
resource,
"has",
entitlement.WithDisplayName("Has Token"),
entitlement.WithDescription("User has a token for an application"),
entitlement.WithAnnotation(&v2.EntitlementImmutable{}),
entitlement.WithGrantableTo(resourceTypeUser),
),
}, "", nil, nil
}

func (u *userTokenResource) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) {
idSplit := strings.Split(resource.Id.Resource, "/")
if len(idSplit) != 2 {
return nil, "", nil, fmt.Errorf("invalid resource id: %s", resource.Id.Resource)
}

userKey := idSplit[0]

grants := []*v2.Grant{
grant.NewGrant(resource, "has", &v2.ResourceId{
Resource: userKey,
ResourceType: resourceTypeUser.Id,
}),
}

return grants, "", nil, nil
}

func (u *userTokenResource) Grant(ctx context.Context, resource *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) {
return nil, nil, fmt.Errorf("granting user tokens is not supported, only revoking")
}

func (u *userTokenResource) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) {
if grant.Principal.Id.ResourceType != resourceTypeUser.Id {
return nil, fmt.Errorf("invalid grant type: %s", grant.Principal.Id.ResourceType)
}

idSplit := strings.Split(grant.Entitlement.Resource.Id.Resource, "/")
if len(idSplit) != 2 {
return nil, fmt.Errorf("invalid resource id: %s", grant.Entitlement.Resource.Id.Resource)
}

userKey := idSplit[0]
clientID := idSplit[1]

err := u.userService.Tokens.Delete(userKey, clientID).Context(ctx).Do()
if err != nil {
return nil, err
}

return nil, nil
}
Loading