blob: e976f0686757d2f02b6351849812ece5ed4a482f [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package util
import (
"fmt"
"strings"
"unicode"
"v.io/v23/context"
"v.io/v23/conventions"
"v.io/v23/naming"
"v.io/v23/query/pattern"
"v.io/v23/security"
"v.io/v23/security/access"
wire "v.io/v23/services/syncbase"
"v.io/v23/verror"
)
// Encode escapes a component name for use in a Syncbase object name. In
// particular, it replaces bytes "%" and "/" with the "%" character followed by
// the byte's two-digit hex code. Clients using the client library need not
// escape names themselves; the client library does so on their behalf.
func Encode(s string) string {
return naming.EncodeAsNameElement(s)
}
// Decode is the inverse of Encode.
func Decode(s string) (string, error) {
res, ok := naming.DecodeFromNameElement(s)
if !ok {
return "", fmt.Errorf("failed to decode component name: %s", s)
}
return res, nil
}
// EncodeId encodes the given Id for use in a Syncbase object name.
func EncodeId(id wire.Id) string {
return Encode(id.String())
}
// DecodeId is the inverse of EncodeId.
func DecodeId(s string) (wire.Id, error) {
dec, err := Decode(s)
if err != nil {
return wire.Id{}, err
}
return wire.ParseId(dec)
}
// Currently we use \xff for perms index storage and \xfe as a component
// separator in storage engine keys. \xfc and \xfd are not used. In the future
// we might use \xfc followed by "c", "d", "e", or "f" as an order-preserving
// 2-byte encoding of our reserved bytes. Note that all valid UTF-8 byte
// sequences not containing the NUL character are allowed.
var reservedBytes = []string{"\x00", "\xfc", "\xfd", "\xfe", "\xff"}
func containsAnyOf(s string, needles []string) bool {
for _, v := range needles {
if strings.Contains(s, v) {
return true
}
}
return false
}
const (
// maxBlessingLen is the max allowed number of bytes in an Id blessing.
// TODO(ivanpi): Blessings are theoretically unbounded in length.
maxBlessingLen = 1024
// maxNameLen is the max allowed number of bytes in an Id name.
maxNameLen = 1024
)
// ValidateId returns nil iff the given Id is a valid database, collection, or
// syncgroup Id.
// TODO(ivanpi): Use verror.New instead of fmt.Errorf everywhere.
func ValidateId(id wire.Id) error {
if x := len([]byte(id.Blessing)); x == 0 {
return fmt.Errorf("Id blessing cannot be empty")
} else if x > maxBlessingLen {
return fmt.Errorf("Id blessing %q exceeds %d bytes", id.Blessing, maxBlessingLen)
}
if x := len([]byte(id.Name)); x == 0 {
return fmt.Errorf("Id name cannot be empty")
} else if x > maxNameLen {
return fmt.Errorf("Id name %q exceeds %d bytes", id.Name, maxNameLen)
}
if bp := security.BlessingPattern(id.Blessing); bp == security.NoExtension {
return fmt.Errorf("Id blessing %q cannot match any blessings, check blessing conventions", id.Blessing)
} else if !bp.IsValid() { // also checks for control characters
return fmt.Errorf("Id blessing %q is not a valid blessing pattern", id.Blessing)
}
if containsAnyOf(id.Blessing, reservedBytes) {
return fmt.Errorf("Id blessing %q contains a reserved byte (one of %q)", id.Blessing, reservedBytes)
}
if strings.IndexFunc(id.Name, unicode.IsControl) != -1 {
return fmt.Errorf("Id name %q contains a control character", id.Name)
}
if containsAnyOf(id.Name, reservedBytes) {
return fmt.Errorf("Id name %q contains a reserved byte (one of %q)", id.Name, reservedBytes)
}
return nil
}
// ValidateRowKey returns nil iff the given string is a valid row key.
func ValidateRowKey(s string) error {
if s == "" {
return fmt.Errorf("row key cannot be empty")
}
if strings.IndexFunc(s, unicode.IsControl) != -1 {
return fmt.Errorf("row key %q contains a control character", s)
}
if containsAnyOf(s, reservedBytes) {
return fmt.Errorf("row key %q contains a reserved byte (one of %q)", s, reservedBytes)
}
return nil
}
// ParseCollectionRowPair splits the "<encCxId>/<rowKey>" part of a Syncbase
// object name into the collection id and the row key or prefix.
func ParseCollectionRowPair(ctx *context.T, pattern string) (wire.Id, string, error) {
parts := strings.SplitN(pattern, "/", 2)
if len(parts) != 2 { // require both collection and row parts
return wire.Id{}, "", verror.New(verror.ErrBadArg, ctx, pattern)
}
collectionEnc, row := parts[0], parts[1]
collection, err := DecodeId(parts[0])
if err == nil {
err = ValidateId(collection)
}
if err != nil {
return wire.Id{}, "", verror.New(wire.ErrInvalidName, ctx, collectionEnc, err)
}
if row != "" {
if err := ValidateRowKey(row); err != nil {
return wire.Id{}, "", verror.New(wire.ErrInvalidName, ctx, row, err)
}
}
return collection, row, nil
}
// RowPrefixPattern returns a CollectionRowPattern matching a single key prefix
// in a single collection.
func RowPrefixPattern(cxId wire.Id, keyPrefix string) wire.CollectionRowPattern {
return wire.CollectionRowPattern{
CollectionBlessing: pattern.Escape(cxId.Blessing),
CollectionName: pattern.Escape(cxId.Name),
RowKey: pattern.Escape(keyPrefix) + "%",
}
}
// PrefixRangeStart returns the start of the row range for the given prefix.
func PrefixRangeStart(p string) string {
return p
}
// PrefixRangeLimit returns the limit of the row range for the given prefix.
func PrefixRangeLimit(p string) string {
// A string is a []byte, i.e. can be thought of as a base-256 number. The code
// below effectively adds 1 to this number, then chops off any trailing \x00
// bytes. If the input string consists entirely of \xff bytes, we return an
// empty string.
x := []byte(p)
for len(x) > 0 {
if x[len(x)-1] == 255 {
x = x[:len(x)-1] // chop off trailing \x00
} else {
x[len(x)-1] += 1 // add 1
break // no carry
}
}
return string(x)
}
// IsPrefix returns true if start and limit together represent a prefix range.
// If true, start is the represented prefix.
func IsPrefix(start, limit string) bool {
return PrefixRangeLimit(start) == limit
}
// AccessController provides access control for various syncbase objects.
type AccessController interface {
// SetPermissions replaces the current Permissions for an object.
// For detailed documentation, see Object.SetPermissions.
SetPermissions(ctx *context.T, perms access.Permissions, version string) error
// GetPermissions returns the current Permissions for an object.
// For detailed documentation, see Object.GetPermissions.
GetPermissions(ctx *context.T) (perms access.Permissions, version string, err error)
}
// AppAndUserPatternFromBlessings infers the app and user blessing pattern from
// the given set of blessing names.
// <idp>:o:<app>:<user> blessings are preferred, with a fallback to
// <idp>:u:<user> and unrestricted app. Returns an error and no-match patterns
// if the inferred pattern is ambiguous (multiple blessings for different apps
// or users are found), or if no blessings matching conventions are found.
// TODO(ivanpi): Allow caller to restrict format to app:user or user instead of
// automatic fallback?
func AppAndUserPatternFromBlessings(blessings ...string) (app, user security.BlessingPattern, err error) {
pbs := conventions.ParseBlessingNames(blessings...)
found := false
// Find a blessing of the form app:user; ensure there is only one app:user pair.
for _, b := range pbs {
a, au := b.AppPattern(), b.AppUserPattern()
if a != security.NoExtension && au != security.NoExtension {
if found && (a != app || au != user) {
return security.NoExtension, security.NoExtension, NewErrFoundMultipleAppUserBlessings(nil, string(user), string(au))
}
app, user, found = a, au, true
}
}
if found {
return app, user, nil
}
// Fall back to a user blessing; ensure there is only one user.
for _, b := range pbs {
u := b.UserPattern()
if u != security.NoExtension {
if found && (u != user) {
return security.NoExtension, security.NoExtension, NewErrFoundMultipleUserBlessings(nil, string(user), string(u))
}
app, user, found = security.AllPrincipals, u, true
}
}
if found {
return app, user, nil
}
// No app:user or user blessings found.
return security.NoExtension, security.NoExtension, NewErrFoundNoConventionalBlessings(nil)
}
// FilterTags returns a copy of the provided perms, filtered to include only
// entries for tags allowed by the allowTags whitelist.
func FilterTags(perms access.Permissions, allowTags ...access.Tag) (filtered access.Permissions) {
filtered = access.Permissions{}
for _, allowTag := range allowTags {
if acl, ok := perms[string(allowTag)]; ok {
filtered[string(allowTag)] = acl
}
}
// Copy to make sure lists in ACLs don't share backing arrays.
return filtered.Copy()
}