You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/iam/scope.go

206 lines
6.0 KiB

package iam
import (
"context"
"errors"
"fmt"
"strings"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"google.golang.org/protobuf/proto"
)
const (
defaultScopeTableName = "iam_scope"
)
// Scope is used to create a hierarchy of "containers" that encompass the scope of
// an IAM resource. Scopes are Global, Orgs and Projects.
type Scope struct {
*store.Scope
// tableName which is used to support overriding the table name in the db
// and making the Scope a ReplayableMessage
tableName string `gorm:"-"`
}
// ensure that Scope implements the interfaces of: Resource, Cloneable, and db.VetForWriter
var _ Resource = (*Scope)(nil)
var _ db.VetForWriter = (*Scope)(nil)
var _ Cloneable = (*Scope)(nil)
func NewOrg(opt ...Option) (*Scope, error) {
global := allocScope()
global.PublicId = "global"
return newScope(&global, opt...)
}
func NewProject(orgPublicId string, opt ...Option) (*Scope, error) {
org := allocScope()
org.PublicId = orgPublicId
p, err := newScope(&org, opt...)
if err != nil {
return nil, fmt.Errorf("error creating new project: %w", err)
}
return p, nil
}
// newScope creates a new Scope with options: WithName specifies the Scope's
// friendly name. WithDescription specifies the scope's description. WithScope
// specifies the Scope's parent and must be filled in. The type of the parent is
// used to determine the type of the child.
func newScope(parent *Scope, opt ...Option) (*Scope, error) {
if parent == nil || parent.PublicId == "" {
return nil, fmt.Errorf("new scope: child scope is missing its parent: %w", db.ErrInvalidParameter)
}
var typ scope.Type
switch {
case parent.PublicId == "global":
typ = scope.Org
case strings.HasPrefix(parent.PublicId, scope.Org.Prefix()):
typ = scope.Project
}
if typ == scope.Unknown {
return nil, fmt.Errorf("new scope: unknown type of scope to create: %w", db.ErrInvalidParameter)
}
opts := getOpts(opt...)
s := &Scope{
Scope: &store.Scope{
Type: typ.String(),
Name: opts.withName,
Description: opts.withDescription,
ParentId: parent.PublicId,
},
}
return s, nil
}
func allocScope() Scope {
return Scope{
Scope: &store.Scope{},
}
}
// Clone creates a clone of the Scope
func (s *Scope) Clone() interface{} {
cp := proto.Clone(s.Scope)
return &Scope{
Scope: cp.(*store.Scope),
}
}
// VetForWrite implements db.VetForWrite() interface for scopes
// this function is intended to be callled by a db.Writer (Create and Update) to validate
// the scope before writing it to the db.
func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
if s.Type == scope.Unknown.String() {
return errors.New("unknown scope type for scope write")
}
if s.PublicId == "" {
return errors.New("public id is empty string for scope write")
}
if opType == db.UpdateOp {
dbOptions := db.GetOpts(opt...)
for _, path := range dbOptions.WithFieldMaskPaths {
switch path {
case "ParentId":
return errors.New("you cannot change a scope's parent")
case "Type":
return errors.New("you cannot change a scope's type")
}
}
}
if opType == db.CreateOp {
switch {
case s.Type == scope.Global.String():
return errors.New("global scope cannot be created")
case s.ParentId == "":
return errors.New("scope must have a parent")
case s.Type == scope.Org.String():
if s.ParentId != "global" {
return errors.New(`org's parent must be "global"`)
}
case s.Type == scope.Project.String():
parentScope := allocScope()
parentScope.PublicId = s.ParentId
if err := r.LookupByPublicId(ctx, &parentScope, opt...); err != nil {
return fmt.Errorf("unable to verify project's org scope: %w", err)
}
if parentScope.Type != scope.Org.String() {
return errors.New("project parent scope is not an org")
}
}
}
return nil
}
// ResourceType returns the type of scope
func (s *Scope) ResourceType() resource.Type {
return resource.Scope
}
// Actions returns the available actions for Scopes
func (*Scope) Actions() map[string]action.Type {
return CrudlActions()
}
// GetScope returns the scope for the "scope" if there is one defined
func (s *Scope) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
if r == nil {
return nil, errors.New("error db is nil for get scope")
}
if s.PublicId == "" {
return nil, errors.New("unable to get scope with unset public id")
}
if s.Type == "" && s.ParentId == "" {
if err := r.LookupByPublicId(ctx, s); err != nil {
return nil, fmt.Errorf("unable to get scope by public id: %w", err)
}
}
// HANDLE_GLOBAL
switch s.Type {
case scope.Global.String():
return nil, nil
default:
var p Scope
switch s.ParentId {
case "":
// no parent id, so use the public_id to find the parent scope. This
// won't work for if the scope hasn't been written to the db yet,
// like during create but in that case the parent id should be set
// for all scopes which are not global, and the global case was
// handled at HANDLE_GLOBAL
where := "public_id in (select parent_id from iam_scope where public_id = ?)"
if err := r.LookupWhere(ctx, &p, where, s.PublicId); err != nil {
return nil, fmt.Errorf("unable to lookup parent public id from public id: %w", err)
}
default:
if err := r.LookupWhere(ctx, &p, "public_id = ?", s.ParentId); err != nil {
return nil, fmt.Errorf("unable to lookup parent from public id: %w", err)
}
}
return &p, nil
}
}
// TableName returns the tablename to override the default gorm table name
func (s *Scope) TableName() string {
if s.tableName != "" {
return s.tableName
}
return defaultScopeTableName
}
// SetTableName sets the tablename and satisfies the ReplayableMessage
// interface. If the caller attempts to set the name to "" the name will be
// reset to the default name.
func (s *Scope) SetTableName(n string) {
s.tableName = n
}