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
3 changes: 3 additions & 0 deletions auth/collection_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ type CollectionChannelAPI interface {

// Returns the CollectionAccess map
GetCollectionsAccess() map[string]map[string]*CollectionAccess

// Returns the ColelctionAccessHistory map
GetCollectionAccessHistory() CollectionAccessHistory
}

// UserCollectionChannelAPI defines the interface for managing channel access that is supported by users but not roles.
Expand Down
2 changes: 2 additions & 0 deletions auth/principal.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type Principal interface {
// Sets the created time for the principal document
SetCreatedAt(t time.Time)

CompactChannelHistory(scope string, col string, channels []string) []string

// Principal includes the PrincipalCollectionAccess interface for operations against
// the _default._default collection (stored directly on the principal for backward
// compatibility)
Expand Down
9 changes: 9 additions & 0 deletions auth/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,15 @@ func (role *roleImpl) authorizeAnyChannel(channels base.Set) error {
return authorizeAnyChannel(role, channels)
}

func (role *roleImpl) CompactChannelHistory(scope, collection string, channels []string) []string {
chanHistory := role.CollectionChannelHistory(scope, collection)
if chanHistory == nil {
return []string{}
}
compactedChannels := chanHistory.PruneHistoryByKey(channels)
return compactedChannels
}

// Returns an HTTP 403 error if the Principal is not allowed to access all the given channels.
// A nil Principal means access control is disabled, so the function will return nil.
func authorizeAllChannels(princ Principal, channels base.Set) error {
Expand Down
28 changes: 28 additions & 0 deletions auth/role_collection_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
package auth

import (
"maps"
"slices"

"github.com/couchbase/sync_gateway/base"
ch "github.com/couchbase/sync_gateway/channels"
)
Expand Down Expand Up @@ -231,3 +234,28 @@ func (role *roleImpl) initChannels(scopeName, collectionName string, channels ba
func (role *roleImpl) GetCollectionsAccess() map[string]map[string]*CollectionAccess {
return role.CollectionsAccess
}

// CollectionAccessHistory maps scope names to collections, each holding the list of channel names with revocation history.
// Shape: scope → collection → []channel
type CollectionAccessHistory map[string]map[string][]string

func (role *roleImpl) GetCollectionAccessHistory() CollectionAccessHistory {

collectionAccess := role.GetCollectionsAccess()
collectionAccessHistoryMap := make(CollectionAccessHistory)
for scope, cols := range collectionAccess {
collectionAccessHistoryMap[scope] = make(map[string][]string)
for col, colVal := range cols {
collectionAccessHistoryMap[scope][col] = slices.Collect(maps.Keys(colVal.ChannelHistory()))
}
}

// Always include the default collection's top-level channel history, without clobbering
// any named collections already recorded under the default scope.
if collectionAccessHistoryMap[base.DefaultScope] == nil {
collectionAccessHistoryMap[base.DefaultScope] = make(map[string][]string)
}
collectionAccessHistoryMap[base.DefaultScope][base.DefaultCollection] = slices.Collect(maps.Keys(role.ChannelHistory()))

return collectionAccessHistoryMap
}
9 changes: 0 additions & 9 deletions auth/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,15 +750,6 @@ func (user *userImpl) GetAddedChannels(channels ch.TimedSet) (base.Set, error) {
return output, nil
}

func (user *userImpl) CompactChannelHistory(scope, collection string, channels []string) []string {
chanHistory := user.CollectionChannelHistory(scope, collection)
if chanHistory == nil {
return []string{}
}
compactedChannels := chanHistory.PruneHistoryByKey(channels)
return compactedChannels
}

// ////// MARSHALING:

// JSON encoding/decoding -- these functions are ugly hacks to work around the current
Expand Down
8 changes: 4 additions & 4 deletions docs/api/admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ paths:
$ref: './paths/admin/db-_user-name-_session.yaml'
'/{db}/_user/{name}/_session/{sessionid}':
$ref: './paths/admin/db-_user-name-_session-sessionid.yaml'
'/{db}/_user/{name}/_history':
$ref: './paths/admin/db-_user-name-_history.yaml'
'/{db}/_user/{name}/_history/compact':
$ref: './paths/admin/db-_user-name-_history-compact.yaml'
'/{db}/_user/{name}/_access_history':
$ref: './paths/admin/db-_user-name-_access_history.yaml'
'/{db}/_user/{name}/_access_history/compact':
$ref: './paths/admin/db-_user-name-_access_history-compact.yaml'
'/{db}/_role/':
$ref: './paths/admin/db-_role-.yaml'
'/{db}/_role/{name}':
Expand Down
39 changes: 26 additions & 13 deletions docs/api/components/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3578,21 +3578,34 @@ RegistryScope:
items:
type: string

CollectionAccessHistory:
description: Channel history organized by scope name. Each scope contains a map of collection names to arrays of channel names.
type: object
additionalProperties:
x-additionalPropertiesName: scope
type: object
additionalProperties:
x-additionalPropertiesName: collection
type: array
items:
type: string
description: Array of channel names with revocation history in this collection.

UserHistory:
description: User history containing various history types organized by scope and collection.
description: User channel history organized by scope and collection.
type: object
title: UserHistory
properties:
channels:
description: Channel history organized by scope and collection.
type: object
additionalProperties:
description: An object keyed by scope name.
type: object
x-additionalPropertiesName: scopename
additionalProperties:
description: An array of channel names for the collection.
type: array
x-additionalPropertiesName: collectionname
items:
type: string
title: UserHistory
allOf:
- $ref: '#/CollectionAccessHistory'
CompactedUserHistory:
description: User channel history organized by scope and collection.
type: object
title: CompactedUserHistory
properties:
compacted_channels:
description: Channel history organized by scope and collection.
allOf:
- $ref: '#/CollectionAccessHistory'
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ post:
content:
application/json:
schema:
$ref: ../../components/schemas.yaml#/UserHistory
$ref: ../../components/schemas.yaml#/CompactedUserHistory
example:
channels:
compacted_channels:
scope1:
collection1:
- scoped_channel2
- scoped_channel1
'400':
description: Bad request. Invalid channel names or malformed request body.
content:
Expand Down
69 changes: 69 additions & 0 deletions rest/admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2511,6 +2511,75 @@ func (h *handler) handleGetClusterInfo() error {
return nil
}

type GetUserAccessHistoryResponse struct {
Channels auth.CollectionAccessHistory `json:"channels"`
}

type CompactUserAccessHistoryResponse struct {
CompactedChannels auth.CollectionAccessHistory `json:"compacted_channels"`
}

func (h *handler) getUserChannelHistory() error {
h.assertAdminOnly()
username := internalUserName(mux.Vars(h.rq)["name"])
user, err := h.db.Authenticator(h.ctx()).GetUser(username)
if err != nil {
return err
}
if user == nil {
return kNotFoundError
}

collectionAccessHistory := user.GetCollectionAccessHistory()

userChannelHistory := GetUserAccessHistoryResponse{
Channels: collectionAccessHistory,
}

h.writeJSON(userChannelHistory)

return err
}

func (h *handler) compactUserChannelHistory() error {
h.assertAdminOnly()
var reqUserChannelHistory GetUserAccessHistoryResponse
err := h.readJSONInto(&reqUserChannelHistory)
if err != nil {
return base.HTTPErrorf(http.StatusBadRequest, "Invalid request payload: %v", err)
}

username := internalUserName(mux.Vars(h.rq)["name"])
authenticator := h.db.Authenticator(h.ctx())
user, err := authenticator.GetUser(username)
if err != nil {
return err
}
if user == nil {
return kNotFoundError
}

colAccessHistoryMap := make(map[string]map[string][]string)
for scope, cols := range reqUserChannelHistory.Channels {
colAccessHistoryMap[scope] = make(map[string][]string)
for col, colVal := range cols {
colAccessHistoryMap[scope][col] = user.CompactChannelHistory(scope, col, colVal)
}
}

userCompactedChannelHistory := CompactUserAccessHistoryResponse{
CompactedChannels: colAccessHistoryMap,
}

err = authenticator.Save(user)
if err != nil {
return err
}

h.writeJSON(userCompactedChannelHistory)
Comment on lines +2574 to +2579
return nil
}

// databaseLoadErrorAsHTTPError converts an error loading a database into an error with an http status code. Pulled into a function so we can duplicate persistent and non persistent config logic.
func databaseLoadErrorAsHTTPError(err error) error {
var httpErr *base.HTTPError
Expand Down
7 changes: 7 additions & 0 deletions rest/revocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,13 @@ func TestRevocationWithAdminChannels(t *testing.T) {

assert.Equal(t, "doc", changes.Results[0].ID)
assert.True(t, changes.Results[0].Revoked)

body := fmt.Sprintf(`{"channels": {%q:{%q:["A"]}}}`, dataStore.ScopeName(), dataStore.CollectionName())
resp = rt.SendAdminRequest("POST", "/db/_user/user/_access_history/compact", body)
RequireStatus(t, resp, http.StatusOK)

changes = rt.WaitForChanges(1, fmt.Sprintf("/{{.keyspace}}/_changes?since=%d&revocations=true", 2), "user", false)
Comment thread
RIT3shSapata marked this conversation as resolved.
assert.Equal(t, "_user/user", changes.Results[0].ID)
}

func TestRevocationWithAdminRoles(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions rest/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).putUser)).Methods("PUT")
dbr.Handle("/_user/{name}",
makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUser)).Methods("DELETE")
dbr.Handle("/_user/{name}/_access_history",
makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).getUserChannelHistory)).Methods("GET")
dbr.Handle("/_user/{name}/_access_history/compact",
makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).compactUserChannelHistory)).Methods("POST")

dbr.Handle("/_user/{name}/_session",
makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUserSessions)).Methods("DELETE")
Expand Down
Loading
Loading