Skip to content

Commit

Permalink
CBG-3807: Diagnostic API - GET user/doc access endpoint (#6892)
Browse files Browse the repository at this point in the history
* Add api routing and handler

* Add 6 test cases and fix implementation

* Add 8 tests and docID option to docGrant in test util

* Add 4 tests and fix handler

* Add test case and attempt to solve deleted role issue

* Add multi doc tests

* add role/chan removal tests and disable deleted role test

* Add documentation for user/doc access endpoint

* Fix sync endpoint conflict and docs

* Add goimports and fix lint

* Fix lint

* Fix getRoleChanEntryOverlap if statements

* Fix deleted role handling

* Remove public chan excess handling

* remove unused function

* Fix TestGetUserDocAccessDynamicRoleChanRemoval2

* Fix goimports lint

* Fix TestGetUserDocAccessDynamicRoleChanRemoval2 again

* Fix conflict

* Disable deleted role tests with named collections
  • Loading branch information
mohammed-madi authored Jul 16, 2024
1 parent a90cbad commit 79b5c2d
Show file tree
Hide file tree
Showing 14 changed files with 1,259 additions and 58 deletions.
10 changes: 5 additions & 5 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (auth *Authenticator) getPrincipal(docID string, factory func() Principal)
changed := false
if !princ.IsDeleted() {
// Check whether any collection's channel list has been invalidated by a doc update -- if so, rebuild it:
channelsChanged, err := auth.rebuildChannels(princ)
channelsChanged, err := auth.RebuildChannels(princ)
if err != nil {
base.WarnfCtx(auth.LogCtx, "RebuildChannels returned error: %v", err)
return nil, nil, false, err
Expand All @@ -190,7 +190,7 @@ func (auth *Authenticator) getPrincipal(docID string, factory func() Principal)
}
if user, ok := princ.(User); ok {
if user.RoleNames() == nil {
if err := auth.rebuildRoles(user); err != nil {
if err := auth.RebuildRoles(user); err != nil {
base.WarnfCtx(auth.LogCtx, "RebuildRoles returned error: %v", err)
return nil, nil, false, err
}
Expand Down Expand Up @@ -227,7 +227,7 @@ func (auth *Authenticator) getPrincipal(docID string, factory func() Principal)
// For each collection in Authenticator.collections:
// - if there is no CollectionAccess on the principal for the collection, rebuilds channels for that collection
// - If CollectionAccess on the principal has been invalidated, rebuilds channels for that collection
func (auth *Authenticator) rebuildChannels(princ Principal) (changed bool, err error) {
func (auth *Authenticator) RebuildChannels(princ Principal) (changed bool, err error) {

changed = false
for scope, collections := range auth.Collections {
Expand Down Expand Up @@ -323,7 +323,7 @@ func (auth *Authenticator) calculateHistory(princName string, invalSeq uint64, i
}

if prunedHistory := currentHistory.PruneHistory(auth.ClientPartitionWindow); len(prunedHistory) > 0 {
base.DebugfCtx(auth.LogCtx, base.KeyCRUD, "rebuildChannels: Pruned principal history on %s for %s", base.UD(princName), base.UD(prunedHistory))
base.DebugfCtx(auth.LogCtx, base.KeyCRUD, "RebuildChannels: Pruned principal history on %s for %s", base.UD(princName), base.UD(prunedHistory))
}

// Ensure no entries are larger than the allowed threshold
Expand Down Expand Up @@ -355,7 +355,7 @@ func CalculateMaxHistoryEntriesPerGrant(channelCount int) int {
return maxEntries
}

func (auth *Authenticator) rebuildRoles(user User) error {
func (auth *Authenticator) RebuildRoles(user User) error {
var roles ch.TimedSet
if auth.channelComputer != nil {
var err error
Expand Down
2 changes: 1 addition & 1 deletion auth/collection_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ type UserCollectionAccess interface {
type CollectionAccess struct {
ExplicitChannels_ ch.TimedSet `json:"admin_channels,omitempty"`
Channels_ ch.TimedSet `json:"all_channels,omitempty"`
ChannelHistory_ TimedSetHistory `json:"channel_history,omitempty"` // Added to when a previously granted channel is revoked. Calculated inside of rebuildChannels.
ChannelHistory_ TimedSetHistory `json:"channel_history,omitempty"` // Added to when a previously granted channel is revoked. Calculated inside of RebuildChannels.
ChannelInvalSeq uint64 `json:"channel_inval_seq,omitempty"` // Sequence at which the channels were invalidated. Data remains in Channels_ for history calculation.
JWTChannels_ ch.TimedSet `json:"jwt_channels,omitempty"` // TODO: JWT properties should only be populated for user but would like to share scope/collection map
JWTLastUpdated *time.Time `json:"jwt_last_updated,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions auth/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1242,8 +1242,8 @@ func TestJWTRolesChannels(t *testing.T) {
user.SetJWTRoles(ch.AtSequence(updates.JWTRoles, user.Sequence()), user.Sequence())
user.SetJWTLastUpdated(time.Now())

require.NoError(t, auth.rebuildRoles(user))
_, rebuildErr := auth.rebuildChannels(user)
require.NoError(t, auth.RebuildRoles(user))
_, rebuildErr := auth.RebuildChannels(user)
require.NoError(t, rebuildErr)

if user.RoleNames() == nil {
Expand Down
6 changes: 3 additions & 3 deletions auth/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type roleImpl struct {
ExplicitChannels_ ch.TimedSet `json:"admin_channels,omitempty"`
Channels_ ch.TimedSet `json:"all_channels"`
Sequence_ uint64 `json:"sequence"`
ChannelHistory_ TimedSetHistory `json:"channel_history,omitempty"` // Added to when a previously granted channel is revoked. Calculated inside of rebuildChannels.
ChannelHistory_ TimedSetHistory `json:"channel_history,omitempty"` // Added to when a previously granted channel is revoked. Calculated inside of RebuildChannels.
ChannelInvalSeq uint64 `json:"channel_inval_seq,omitempty"` // Sequence at which the channels were invalidated. Data remains in Channels_ for history calculation.
Deleted bool `json:"deleted,omitempty"`
CollectionsAccess map[string]map[string]*CollectionAccess `json:"collection_access,omitempty"` // Nested maps of CollectionAccess, indexed by scope and collection name
Expand Down Expand Up @@ -232,7 +232,7 @@ func (auth *Authenticator) NewRole(name string, channels base.Set) (Role, error)
if err := role.initRole(name, channels, auth.Collections); err != nil {
return nil, err
}
if _, err := auth.rebuildChannels(role); err != nil {
if _, err := auth.RebuildChannels(role); err != nil {
return nil, err
}
return role, nil
Expand All @@ -256,7 +256,7 @@ func (auth *Authenticator) NewRoleNoChannels(name string) (Role, error) {
if err := role.initRole(name, nil, nil); err != nil {
return nil, err
}
if _, err := auth.rebuildChannels(role); err != nil {
if _, err := auth.RebuildChannels(role); err != nil {
return nil, err
}
return role, nil
Expand Down
10 changes: 5 additions & 5 deletions auth/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type userImplBody struct {
JWTLastUpdated_ time.Time `json:"jwt_last_updated,omitempty"`
RolesSince_ ch.TimedSet `json:"rolesSince"`
RoleInvalSeq uint64 `json:"role_inval_seq,omitempty"` // Sequence at which the roles were invalidated. Data remains in RolesSince_ for history calculation.
RoleHistory_ TimedSetHistory `json:"role_history,omitempty"` // Added to when a previously granted role is revoked. Calculated inside of rebuildRoles.
RoleHistory_ TimedSetHistory `json:"role_history,omitempty"` // Added to when a previously granted role is revoked. Calculated inside of RebuildRoles.
SessionUUID_ string `json:"session_uuid"` // marker of when the user object changes, to match with session docs to determine if they are valid

OldExplicitRoles_ []string `json:"admin_roles,omitempty"` // obsolete; declared for migration
Expand Down Expand Up @@ -97,11 +97,11 @@ func (auth *Authenticator) NewUser(username string, password string, channels ba
return nil, err
}

if _, err := auth.rebuildChannels(user); err != nil {
if _, err := auth.RebuildChannels(user); err != nil {
return nil, err
}

if err := auth.rebuildRoles(user); err != nil {
if err := auth.RebuildRoles(user); err != nil {
return nil, err
}

Expand All @@ -126,11 +126,11 @@ func (auth *Authenticator) NewUserNoChannels(username string, password string) (
if err := user.initRole(username, nil, nil); err != nil {
return nil, err
}
if _, err := auth.rebuildChannels(user); err != nil {
if _, err := auth.RebuildChannels(user); err != nil {
return nil, err
}

if err := auth.rebuildRoles(user); err != nil {
if err := auth.RebuildRoles(user); err != nil {
return nil, err
}

Expand Down
6 changes: 6 additions & 0 deletions docs/api/components/responses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,9 @@ All_user_channels_response:
application/json:
schema:
$ref: ./schemas.yaml#/all_user_channels
user_access_span_response:
description: Grant history entries for a user, showing which documents the user had access to, through which channels and for which sequence spans..
content:
application/json:
schema:
$ref: ./schemas.yaml#/doc_access_spans
17 changes: 17 additions & 0 deletions docs/api/components/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2691,3 +2691,20 @@ channelEntry:
updated_at:
type: integer
description: Unix timestamp of last update
doc_access_spans:
description: |-
Grant history entries for a user, showing which documents the user had access to, through which channels and for which sequence spans.
type: object
properties:
doc_id:
description: |-
Document names.
type: object
properties:
channel:
description: Channel name
type: object
properties:
entries:
type: array
description: Start sequence to end sequence. If the channel is currently granted, the end sequence will be zero.
2 changes: 2 additions & 0 deletions docs/api/diagnostic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ paths:
$ref: './paths/diagnostic/keyspace-import_filter.yaml'
'/{db}/_user/{name}/all_channels':
$ref: './paths/diagnostic/db-_user-name-_all_channels.yaml'
'/{keyspace}/_user/{name}':
$ref: './paths/diagnostic/keyspace-_user-name.yaml'
externalDocs:
description: Sync Gateway Quickstart | Couchbase Docs
url: 'https://docs.couchbase.com/sync-gateway/current/index.html'
33 changes: 33 additions & 0 deletions docs/api/paths/diagnostic/keyspace-_user-name.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2022-Present Couchbase, Inc.
#
# Use of this software is governed by the Business Source License included
# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
# in that file, in accordance with the Business Source License, use of this
# software will be governed by the Apache License, Version 2.0, included in
# the file licenses/APL2.txt.
parameters:
- $ref: ../../components/parameters.yaml#/keyspace
- $ref: ../../components/parameters.yaml#/user-name
get:
summary: Get all document access spans.
description: |-
Retrieve all sequence spans where a user had access to a list of documents.
The spans are in the form of start sequence - end sequence.
Required Sync Gateway RBAC roles:
* Sync Gateway Architect
* Sync Gateway Application
* Sync Gateway Application Read Only
parameters:
- $ref: ../../components/parameters.yaml#/doc_id
responses:
'200':
$ref: ../../components/responses.yaml#/user_access_span_response
'400':
description: Bad Request
'404':
$ref: ../../components/responses.yaml#/Not-found
tags:
- Database Security
operationId: get_keyspace-_user-name
Loading

0 comments on commit 79b5c2d

Please sign in to comment.