open-vault/vendor/github.com/couchbase/gocb/v2/cluster_usermgr.go

793 lines
19 KiB
Go

package gocb
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// AuthDomain specifies the user domain of a specific user
type AuthDomain string
const (
// LocalDomain specifies users that are locally stored in Couchbase.
LocalDomain AuthDomain = "local"
// ExternalDomain specifies users that are externally stored
// (in LDAP for instance).
ExternalDomain AuthDomain = "external"
)
type jsonOrigin struct {
Type string `json:"type"`
Name string `json:"name"`
}
type jsonRole struct {
RoleName string `json:"role"`
BucketName string `json:"bucket_name"`
}
type jsonRoleDescription struct {
jsonRole
Name string `json:"name"`
Description string `json:"desc"`
}
type jsonRoleOrigins struct {
jsonRole
Origins []jsonOrigin
}
type jsonUserMetadata struct {
ID string `json:"id"`
Name string `json:"name"`
Roles []jsonRoleOrigins `json:"roles"`
Groups []string `json:"groups"`
Domain AuthDomain `json:"domain"`
ExternalGroups []string `json:"external_groups"`
PasswordChanged time.Time `json:"password_change_date"`
}
type jsonGroup struct {
Name string `json:"id"`
Description string `json:"description"`
Roles []jsonRole `json:"roles"`
LDAPGroupReference string `json:"ldap_group_ref"`
}
// Role represents a specific permission.
type Role struct {
Name string `json:"role"`
Bucket string `json:"bucket_name"`
}
func (ro *Role) fromData(data jsonRole) error {
ro.Name = data.RoleName
ro.Bucket = data.BucketName
return nil
}
// RoleAndDescription represents a role with its display name and description.
type RoleAndDescription struct {
Role
DisplayName string
Description string
}
func (rd *RoleAndDescription) fromData(data jsonRoleDescription) error {
err := rd.Role.fromData(data.jsonRole)
if err != nil {
return err
}
rd.DisplayName = data.Name
rd.Description = data.Description
return nil
}
// Origin indicates why a user has a specific role. Is the Origin Type is "user" then the role is assigned
// directly to the user. If the type is "group" then it means that the role has been inherited from the group
// identified by the Name field.
type Origin struct {
Type string
Name string
}
func (o *Origin) fromData(data jsonOrigin) error {
o.Type = data.Type
o.Name = data.Name
return nil
}
// RoleAndOrigins associates a role with its origins.
type RoleAndOrigins struct {
Role
Origins []Origin
}
func (ro *RoleAndOrigins) fromData(data jsonRoleOrigins) error {
err := ro.Role.fromData(data.jsonRole)
if err != nil {
return err
}
origins := make([]Origin, len(data.Origins))
for _, originData := range data.Origins {
var origin Origin
err := origin.fromData(originData)
if err != nil {
return err
}
origins = append(origins, origin)
}
ro.Origins = origins
return nil
}
// User represents a user which was retrieved from the server.
type User struct {
Username string
DisplayName string
// Roles are the roles assigned to the user that are of type "user".
Roles []Role
Groups []string
Password string
}
// UserAndMetadata represents a user and user meta-data from the server.
type UserAndMetadata struct {
User
Domain AuthDomain
// EffectiveRoles are all of the user's roles and the origins.
EffectiveRoles []RoleAndOrigins
ExternalGroups []string
PasswordChanged time.Time
}
func (um *UserAndMetadata) fromData(data jsonUserMetadata) error {
um.User.Username = data.ID
um.User.DisplayName = data.Name
um.User.Groups = data.Groups
um.ExternalGroups = data.ExternalGroups
um.Domain = data.Domain
um.PasswordChanged = data.PasswordChanged
var roles []Role
var effectiveRoles []RoleAndOrigins
for _, roleData := range data.Roles {
var effectiveRole RoleAndOrigins
err := effectiveRole.fromData(roleData)
if err != nil {
return err
}
effectiveRoles = append(effectiveRoles, effectiveRole)
role := effectiveRole.Role
if roleData.Origins == nil {
roles = append(roles, role)
} else {
for _, origin := range effectiveRole.Origins {
if origin.Type == "user" {
roles = append(roles, role)
break
}
}
}
}
um.EffectiveRoles = effectiveRoles
um.User.Roles = roles
return nil
}
// Group represents a user group on the server.
type Group struct {
Name string
Description string
Roles []Role
LDAPGroupReference string
}
func (g *Group) fromData(data jsonGroup) error {
g.Name = data.Name
g.Description = data.Description
g.LDAPGroupReference = data.LDAPGroupReference
roles := make([]Role, len(data.Roles))
for roleIdx, roleData := range data.Roles {
err := roles[roleIdx].fromData(roleData)
if err != nil {
return err
}
}
g.Roles = roles
return nil
}
// UserManager provides methods for performing Couchbase user management.
type UserManager struct {
provider mgmtProvider
tracer requestTracer
}
func (um *UserManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
logDebugf("Failed to read search index response body: %s", err)
return nil
}
var bodyErr error
if resp.StatusCode == 404 {
if strings.Contains(strings.ToLower(string(b)), "unknown user") {
bodyErr = ErrUserNotFound
} else if strings.Contains(strings.ToLower(string(b)), "user was not found") {
bodyErr = ErrUserNotFound
} else if strings.Contains(strings.ToLower(string(b)), "group was not found") {
bodyErr = ErrGroupNotFound
} else if strings.Contains(strings.ToLower(string(b)), "unknown group") {
bodyErr = ErrGroupNotFound
} else {
bodyErr = errors.New(string(b))
}
} else {
bodyErr = errors.New(string(b))
}
return makeGenericMgmtError(bodyErr, req, resp)
}
// GetAllUsersOptions is the set of options available to the user manager GetAll operation.
type GetAllUsersOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
DomainName string
}
// GetAllUsers returns a list of all the users from the cluster.
func (um *UserManager) GetAllUsers(opts *GetAllUsersOptions) ([]UserAndMetadata, error) {
if opts == nil {
opts = &GetAllUsersOptions{}
}
span := um.tracer.StartSpan("GetAllUsers", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
if opts.DomainName == "" {
opts.DomainName = string(LocalDomain)
}
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "GET",
Path: fmt.Sprintf("/settings/rbac/users/%s", opts.DomainName),
IsIdempotent: true,
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return nil, makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return nil, usrErr
}
return nil, makeMgmtBadStatusError("failed to get users", &req, resp)
}
var usersData []jsonUserMetadata
jsonDec := json.NewDecoder(resp.Body)
err = jsonDec.Decode(&usersData)
if err != nil {
return nil, err
}
users := make([]UserAndMetadata, len(usersData))
for userIdx, userData := range usersData {
err := users[userIdx].fromData(userData)
if err != nil {
return nil, err
}
}
return users, nil
}
// GetUserOptions is the set of options available to the user manager Get operation.
type GetUserOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
DomainName string
}
// GetUser returns the data for a particular user
func (um *UserManager) GetUser(name string, opts *GetUserOptions) (*UserAndMetadata, error) {
if opts == nil {
opts = &GetUserOptions{}
}
span := um.tracer.StartSpan("GetUser", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
if opts.DomainName == "" {
opts.DomainName = string(LocalDomain)
}
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "GET",
Path: fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, name),
IsIdempotent: true,
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return nil, makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return nil, usrErr
}
return nil, makeMgmtBadStatusError("failed to get user", &req, resp)
}
var userData jsonUserMetadata
jsonDec := json.NewDecoder(resp.Body)
err = jsonDec.Decode(&userData)
if err != nil {
return nil, err
}
var user UserAndMetadata
err = user.fromData(userData)
if err != nil {
return nil, err
}
return &user, nil
}
// UpsertUserOptions is the set of options available to the user manager Upsert operation.
type UpsertUserOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
DomainName string
}
// UpsertUser updates a built-in RBAC user on the cluster.
func (um *UserManager) UpsertUser(user User, opts *UpsertUserOptions) error {
if opts == nil {
opts = &UpsertUserOptions{}
}
span := um.tracer.StartSpan("UpsertUser", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
if opts.DomainName == "" {
opts.DomainName = string(LocalDomain)
}
var reqRoleStrs []string
for _, roleData := range user.Roles {
if roleData.Bucket == "" {
reqRoleStrs = append(reqRoleStrs, roleData.Name)
} else {
reqRoleStrs = append(reqRoleStrs, fmt.Sprintf("%s[%s]", roleData.Name, roleData.Bucket))
}
}
reqForm := make(url.Values)
reqForm.Add("name", user.DisplayName)
if user.Password != "" {
reqForm.Add("password", user.Password)
}
if len(user.Groups) > 0 {
reqForm.Add("groups", strings.Join(user.Groups, ","))
}
reqForm.Add("roles", strings.Join(reqRoleStrs, ","))
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "PUT",
Path: fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, user.Username),
Body: []byte(reqForm.Encode()),
ContentType: "application/x-www-form-urlencoded",
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return usrErr
}
return makeMgmtBadStatusError("failed to upsert user", &req, resp)
}
return nil
}
// DropUserOptions is the set of options available to the user manager Drop operation.
type DropUserOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
DomainName string
}
// DropUser removes a built-in RBAC user on the cluster.
func (um *UserManager) DropUser(name string, opts *DropUserOptions) error {
if opts == nil {
opts = &DropUserOptions{}
}
span := um.tracer.StartSpan("DropUser", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
if opts.DomainName == "" {
opts.DomainName = string(LocalDomain)
}
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "DELETE",
Path: fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, name),
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return usrErr
}
return makeMgmtBadStatusError("failed to drop user", &req, resp)
}
return nil
}
// GetRolesOptions is the set of options available to the user manager GetRoles operation.
type GetRolesOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
}
// GetRoles lists the roles supported by the cluster.
func (um *UserManager) GetRoles(opts *GetRolesOptions) ([]RoleAndDescription, error) {
if opts == nil {
opts = &GetRolesOptions{}
}
span := um.tracer.StartSpan("GetRoles", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "GET",
Path: "/settings/rbac/roles",
RetryStrategy: opts.RetryStrategy,
IsIdempotent: true,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return nil, makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return nil, usrErr
}
return nil, makeMgmtBadStatusError("failed to get roles", &req, resp)
}
var roleDatas []jsonRoleDescription
jsonDec := json.NewDecoder(resp.Body)
err = jsonDec.Decode(&roleDatas)
if err != nil {
return nil, err
}
roles := make([]RoleAndDescription, len(roleDatas))
for roleIdx, roleData := range roleDatas {
err := roles[roleIdx].fromData(roleData)
if err != nil {
return nil, err
}
}
return roles, nil
}
// GetGroupOptions is the set of options available to the group manager Get operation.
type GetGroupOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
}
// GetGroup fetches a single group from the server.
func (um *UserManager) GetGroup(groupName string, opts *GetGroupOptions) (*Group, error) {
if groupName == "" {
return nil, makeInvalidArgumentsError("groupName cannot be empty")
}
if opts == nil {
opts = &GetGroupOptions{}
}
span := um.tracer.StartSpan("GetGroup", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "GET",
Path: fmt.Sprintf("/settings/rbac/groups/%s", groupName),
RetryStrategy: opts.RetryStrategy,
IsIdempotent: true,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return nil, makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return nil, usrErr
}
return nil, makeMgmtBadStatusError("failed to get group", &req, resp)
}
var groupData jsonGroup
jsonDec := json.NewDecoder(resp.Body)
err = jsonDec.Decode(&groupData)
if err != nil {
return nil, err
}
var group Group
err = group.fromData(groupData)
if err != nil {
return nil, err
}
return &group, nil
}
// GetAllGroupsOptions is the set of options available to the group manager GetAll operation.
type GetAllGroupsOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
}
// GetAllGroups fetches all groups from the server.
func (um *UserManager) GetAllGroups(opts *GetAllGroupsOptions) ([]Group, error) {
if opts == nil {
opts = &GetAllGroupsOptions{}
}
span := um.tracer.StartSpan("GetAllGroups", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "GET",
Path: "/settings/rbac/groups",
RetryStrategy: opts.RetryStrategy,
IsIdempotent: true,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return nil, makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return nil, usrErr
}
return nil, makeMgmtBadStatusError("failed to get all groups", &req, resp)
}
var groupDatas []jsonGroup
jsonDec := json.NewDecoder(resp.Body)
err = jsonDec.Decode(&groupDatas)
if err != nil {
return nil, err
}
groups := make([]Group, len(groupDatas))
for groupIdx, groupData := range groupDatas {
err = groups[groupIdx].fromData(groupData)
if err != nil {
return nil, err
}
}
return groups, nil
}
// UpsertGroupOptions is the set of options available to the group manager Upsert operation.
type UpsertGroupOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
}
// UpsertGroup creates, or updates, a group on the server.
func (um *UserManager) UpsertGroup(group Group, opts *UpsertGroupOptions) error {
if group.Name == "" {
return makeInvalidArgumentsError("group name cannot be empty")
}
if opts == nil {
opts = &UpsertGroupOptions{}
}
span := um.tracer.StartSpan("UpsertGroup", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
var reqRoleStrs []string
for _, roleData := range group.Roles {
if roleData.Bucket == "" {
reqRoleStrs = append(reqRoleStrs, roleData.Name)
} else {
reqRoleStrs = append(reqRoleStrs, fmt.Sprintf("%s[%s]", roleData.Name, roleData.Bucket))
}
}
reqForm := make(url.Values)
reqForm.Add("description", group.Description)
reqForm.Add("ldap_group_ref", group.LDAPGroupReference)
reqForm.Add("roles", strings.Join(reqRoleStrs, ","))
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "PUT",
Path: fmt.Sprintf("/settings/rbac/groups/%s", group.Name),
Body: []byte(reqForm.Encode()),
ContentType: "application/x-www-form-urlencoded",
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return usrErr
}
return makeMgmtBadStatusError("failed to upsert group", &req, resp)
}
return nil
}
// DropGroupOptions is the set of options available to the group manager Drop operation.
type DropGroupOptions struct {
Timeout time.Duration
RetryStrategy RetryStrategy
}
// DropGroup removes a group from the server.
func (um *UserManager) DropGroup(groupName string, opts *DropGroupOptions) error {
if groupName == "" {
return makeInvalidArgumentsError("groupName cannot be empty")
}
if opts == nil {
opts = &DropGroupOptions{}
}
span := um.tracer.StartSpan("DropGroup", nil).
SetTag("couchbase.service", "mgmt")
defer span.Finish()
req := mgmtRequest{
Service: ServiceTypeManagement,
Method: "DELETE",
Path: fmt.Sprintf("/settings/rbac/groups/%s", groupName),
RetryStrategy: opts.RetryStrategy,
UniqueID: uuid.New().String(),
Timeout: opts.Timeout,
parentSpan: span.Context(),
}
resp, err := um.provider.executeMgmtRequest(req)
if err != nil {
return makeGenericMgmtError(err, &req, resp)
}
defer ensureBodyClosed(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
usrErr := um.tryParseErrorMessage(&req, resp)
if usrErr != nil {
return usrErr
}
return makeMgmtBadStatusError("failed to drop group", &req, resp)
}
return nil
}