mirror of https://github.com/hashicorp/boundary
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.
251 lines
7.8 KiB
251 lines
7.8 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package iam
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db/timestamp"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/iam/store"
|
|
"github.com/hashicorp/boundary/internal/oplog"
|
|
"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
|
|
|
|
StoragePolicyId string `json:"storage_policy_id,omitempty" gorm:"-"`
|
|
|
|
// 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)
|
|
_ db.VetForWriter = (*Scope)(nil)
|
|
_ Cloneable = (*Scope)(nil)
|
|
)
|
|
|
|
func NewOrg(ctx context.Context, opt ...Option) (*Scope, error) {
|
|
global := AllocScope()
|
|
global.PublicId = scope.Global.String()
|
|
return newScope(ctx, &global, opt...)
|
|
}
|
|
|
|
func NewProject(ctx context.Context, orgPublicId string, opt ...Option) (*Scope, error) {
|
|
const op = "iam.NewProject"
|
|
org := AllocScope()
|
|
org.PublicId = orgPublicId
|
|
p, err := newScope(ctx, &org, opt...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
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. WithPrimaryAuthMethodId specifies
|
|
// the primary auth method for the scope
|
|
func newScope(ctx context.Context, parent *Scope, opt ...Option) (*Scope, error) {
|
|
const op = "iam.newScope"
|
|
if parent == nil || parent.PublicId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "child scope is missing its parent")
|
|
}
|
|
var typ scope.Type
|
|
switch {
|
|
case parent.PublicId == scope.Global.String():
|
|
typ = scope.Org
|
|
case strings.HasPrefix(parent.PublicId, scope.Org.Prefix()):
|
|
typ = scope.Project
|
|
}
|
|
if typ == scope.Unknown {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown scope type")
|
|
}
|
|
|
|
opts := getOpts(opt...)
|
|
s := &Scope{
|
|
Scope: &store.Scope{
|
|
Type: typ.String(),
|
|
Name: opts.withName,
|
|
Description: opts.withDescription,
|
|
ParentId: parent.PublicId,
|
|
PrimaryAuthMethodId: opts.withPrimaryAuthMethodId,
|
|
},
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func AllocScope() Scope {
|
|
return Scope{
|
|
Scope: &store.Scope{},
|
|
}
|
|
}
|
|
|
|
// Clone creates a clone of the Scope
|
|
func (s *Scope) Clone() any {
|
|
cp := proto.Clone(s.Scope)
|
|
return &Scope{
|
|
Scope: cp.(*store.Scope),
|
|
}
|
|
}
|
|
|
|
// Oplog provides the oplog.Metadata for recording operations taken on a Scope.
|
|
func (s *Scope) Oplog(op oplog.OpType) oplog.Metadata {
|
|
return oplog.Metadata{
|
|
"resource-public-id": []string{s.PublicId},
|
|
"resource-type": []string{"scope"},
|
|
"op-type": []string{op.String()},
|
|
"parent-id": []string{s.ParentId},
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
const op = "iam.(Scope).VetForWrite"
|
|
if s.Type == scope.Unknown.String() {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "unknown scope type")
|
|
}
|
|
if s.PublicId == "" {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
|
}
|
|
if opType == db.UpdateOp {
|
|
dbOptions := db.GetOpts(opt...)
|
|
for _, path := range dbOptions.WithFieldMaskPaths {
|
|
switch path {
|
|
case "ParentId":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "you cannot change a scope's parent")
|
|
case "Type":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "you cannot change a scope's type")
|
|
}
|
|
}
|
|
}
|
|
if opType == db.CreateOp {
|
|
switch {
|
|
case s.Type == scope.Global.String():
|
|
return errors.New(ctx, errors.InvalidParameter, op, "you cannot create a global scope")
|
|
case s.ParentId == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "scope must have a parent")
|
|
case s.Type == scope.Org.String():
|
|
if s.ParentId != scope.Global.String() {
|
|
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf(`org's parent must be "%s"`, scope.Global.String()))
|
|
}
|
|
case s.Type == scope.Project.String():
|
|
parentScope := AllocScope()
|
|
parentScope.PublicId = s.ParentId
|
|
if err := r.LookupByPublicId(ctx, &parentScope, opt...); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to verify project's org scope"))
|
|
}
|
|
if parentScope.Type != scope.Org.String() {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "project parent scope is not an org")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetResourceType returns the type of scope
|
|
func (s *Scope) GetResourceType() 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) {
|
|
const op = "iam.(Scope).GetScope"
|
|
if r == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil reader")
|
|
}
|
|
if s.PublicId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
|
}
|
|
if s.Type == "" && s.ParentId == "" {
|
|
if err := r.LookupByPublicId(ctx, s); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
// 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, []any{s.PublicId}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup parent public id from public id"))
|
|
}
|
|
default:
|
|
if err := r.LookupWhere(ctx, &p, "public_id = ?", []any{s.ParentId}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup parent from public id"))
|
|
}
|
|
}
|
|
return &p, nil
|
|
}
|
|
}
|
|
|
|
// GetStoragePolicyId returns the storage policy id attached to the scope
|
|
func (s *Scope) GetStoragePolicyId() string {
|
|
return s.StoragePolicyId
|
|
}
|
|
|
|
// SetStoragePolicyId sets the storage policy id
|
|
func (s *Scope) SetStoragePolicyId(v string) {
|
|
s.StoragePolicyId = v
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type deletedScope struct {
|
|
PublicId string `gorm:"primary_key"`
|
|
DeleteTime *timestamp.Timestamp
|
|
}
|
|
|
|
// TableName returns the tablename to override the default gorm table name
|
|
func (s *deletedScope) TableName() string {
|
|
return "iam_scope_deleted"
|
|
}
|