Skip to main content

Overview

Ubicloud implements Attribute-Based Access Control (ABAC) to provide fine-grained permissions for users and resources. ABAC uses three core concepts: subjects (who), actions (what), and objects (which resources) to define flexible access policies. From the README:
Attribute-Based Access Control (ABAC): With ABAC, you can define attributes, roles, and permissions for users and give them fine-grained access to resources. You can read more about our ABAC design here.

Core Components

ABAC in Ubicloud consists of three types of tags and access control entries:
  • Subject Tags: Represent who is performing the action (users, accounts, API keys)
  • Action Tags: Represent what operation is being performed (view, create, delete)
  • Object Tags: Represent which resources are being accessed (VMs, subnets, endpoints)
  • Access Control Entries: Connect subjects, actions, and objects to grant permissions

Access Control Entry Model

class AccessControlEntry < Sequel::Model
  many_to_one :project, read_only: true
  plugin ResourceMethods

  def_column_alias :object_id, :object_id  # Use __id__ for internal object id
end

Attributes

AttributeTypeDescription
idUUIDUnique identifier
project_idUUIDAssociated project
subject_idUUIDSubject tag or entity performing the action
action_idUUIDAction tag or action type (optional)
object_idUUIDObject tag or resource being accessed (optional)
When action_id or object_id are null, the entry grants access to all actions or all objects respectively.

Subject Tags

Subject tags represent entities that can perform actions:
class SubjectTag < Sequel::Model
  plugin ResourceMethods
  include AccessControlModelTag

  module Cleanup
    def before_destroy
      AccessControlEntry.where(subject_id: id).destroy
      DB[:applied_subject_tag].where(subject_id: id).delete
      super
    end
  end
end

Valid Subject Members

def self.valid_member?(project_id, subject)
  case subject
  when SubjectTag
    subject.project_id == project_id
  when Account
    !DB[:access_tag].where(project_id: project_id, hyper_tag_id: subject.id).empty?
  when ApiKey
    subject.owner_table == "accounts" && subject.project_id == project_id
  end
end
Subjects can be:
  • Subject Tags: Groups of users or API keys
  • Accounts: Individual user accounts
  • API Keys: Personal access tokens

Admin Tag

Every project has a special “Admin” subject tag with full permissions:
def self.admin_tag?(id)
  !where(id: id, name: "Admin").empty?
end
The Admin subject tag cannot be included in other tags or deleted.

Action Tags

Action tags represent operations that can be performed:
class ActionTag < Sequel::Model
  plugin ResourceMethods
  include AccessControlModelTag

  MEMBER_ID = "ffffffff-ff00-834a-87ff-ff828ea2dd80"

  dataset_module do
    where :global, project_id: nil
    order :by_name, :name

    def global_by_name
      global.by_name
    end
  end

  plugin :subset_static_cache
  cache_subset :global_by_name
end

Global vs Project-Specific Actions

Actions can be:
  • Global: project_id is nil, available across all projects
  • Project-specific: project_id is set, only available in that project
def self.valid_member?(project_id, action)
  case action
  when ActionTag
    action.project_id == project_id || !action.project_id
  when ActionType
    true
  end
end

Action Types

Predefined action types include:
  • view - Read access
  • create - Create new resources
  • edit - Modify existing resources
  • delete - Remove resources
  • update - Update resource configuration

Object Tags

Object tags represent resources that can be accessed:
class ObjectTag < Sequel::Model
  plugin ResourceMethods
  include AccessControlModelTag

  module Cleanup
    def before_destroy
      AccessControlEntry.where(object_id: id).destroy
      DB[:applied_object_tag].where(object_id: id).delete
      super
    end
  end
end

Valid Object Members

def self.valid_member?(project_id, object)
  case object
  when ObjectTag, ObjectMetatag, SubjectTag, ActionTag, InferenceEndpoint, Vm, PrivateSubnet, PostgresResource, Firewall, LoadBalancer
    object.project_id == project_id
  when Project
    object.id == project_id
  when ApiKey
    object.owner_table == "project" && object.owner_id == project_id
  end
end
Objects can be:
  • Object Tags: Groups of resources
  • Direct Resources: VMs, subnets, databases, endpoints, etc.
  • Project: The project itself
  • API Keys: Inference endpoint API keys
  • Other Tags: Subject tags, action tags (for meta-permissions)

Metatags

Object tags have metatags for controlling access to the tag itself:
def metatag
  ObjectMetatag.new(self)
end

def metatag_ubid
  ObjectMetatag.to_meta(ubid)
end

def metatag_uuid
  UBID.to_uuid(metatag_ubid)
end
Granting access to a tag’s metatag allows managing the tag itself, not the objects it contains.

Tag Membership Management

Tags can contain other tags or entities, creating hierarchies:
module AccessControlModelTag
  def add_member(member_id)
    applied_dataset.insert(:tag_id => id, applied_column => member_id)
  end

  def member_ids
    applied_dataset.where(tag_id: id).select_map(applied_column)
  end

  def add_members(member_ids)
    applied_dataset.import([:tag_id, applied_column], member_ids.map { [id, it] })
  end

  def remove_members(member_ids)
    applied_dataset.where(:tag_id => id, applied_column => member_ids).delete
  end
end

Recursive Tag Inclusion

Tags can include other tags, but recursion is prevented:
def currently_included_in
  DB[:tag]
    .with_recursive(:tag,
      DB[applied_table].where(applied_column => id).select(:tag_id, 0),
      DB[applied_table].join(:tag, tag_id: applied_column)
        .select(Sequel[applied_table][:tag_id], Sequel[:level] + 1)
        .where { level < Config.recursive_tag_limit },
      args: [:tag_id, :level])
    .select_map(:tag_id)
end
The recursive tag limit is configured via Config.recursive_tag_limit to prevent infinite loops.

Validation Rules

def check_members_to_add(to_add)
  issues = []
  current_members = member_ids

  # Prevent simple recursion
  if to_add.include?(id)
    to_add.delete(id)
    issues << "cannot include tag in itself"
  end

  # Remove duplicates
  size = to_add.size
  to_add -= current_members
  if to_add.size != size
    issues << "#{size - to_add.size} members already in tag"
  end

  # Prevent complex recursion
  size = to_add.size
  to_add -= currently_included_in
  if to_add.size != size
    issues << "#{size - to_add.size} members already include tag directly or indirectly"
  end

  # Validate member types
  proposed_additions = {}
  to_add.each { proposed_additions[it] = nil }
  UBID.resolve_map(proposed_additions)

  to_add = []
  proposed_additions.each_value do |it|
    if is_a?(SubjectTag) && it.is_a?(SubjectTag) && it.name == "Admin"
      issues << "cannot include Admin subject tag in another tag"
      next
    end
    to_add << it if it && self.class.valid_member?(project_id, it)
  end
  
  if proposed_additions.size != to_add.size
    issues << "#{proposed_additions.size - to_add.size} members not valid"
  end

  to_add.map!(&:id)
  to_add.uniq!
  [to_add, issues]
end

Creating Access Control Entries

Basic ACE Creation

Grant a subject permission to perform an action on an object:
ace = AccessControlEntry.create(
  project_id: project.id,
  subject_id: subject_tag.id,
  action_id: action_tag.id,
  object_id: object_tag.id
)

Wildcard Permissions

Grant access to all actions:
ace = AccessControlEntry.create(
  project_id: project.id,
  subject_id: subject_tag.id,
  action_id: nil,              # All actions
  object_id: inference_endpoint.id
)
Grant access to all objects:
ace = AccessControlEntry.create(
  project_id: project.id,
  subject_id: subject_tag.id,
  action_id: view_action.id,
  object_id: nil               # All objects
)

Using UBIDs

Update an ACE using UBIDs instead of UUIDs:
ace.update_from_ubids({
  subject_id: "st8abc123def456",
  action_id: "at8xyz789ghi012",
  object_id: "ot8uvw345jkl678"
})
Implementation:
def update_from_ubids(hash)
  update(hash.transform_values { UBID.to_uuid(it) if it })
end

Validation

ACEs are validated to ensure all IDs belong to the project:
def validate
  if project_id
    {subject_id: subject_id, action_id: action_id, object_id: object_id}.each do |field, value|
      next unless value

      ubid = UBID.from_uuidish(value).to_s

      model = case field
      when :subject_id
        SubjectTag
      when :action_id
        ActionTag
      else
        ObjectTag
      end

      object = ubid.start_with?("et") ? ApiKey.with_pk(value) : UBID.decode(ubid)
      unless model.valid_member?(project_id, object)
        errors.add(field, "is not related to this project")
      end
    end
  end

  super
end
1

Check field presence

Skip validation if the field is nil
2

Decode UBID

Convert UUID to UBID string for decoding
3

Determine model type

Select the appropriate tag model based on the field
4

Validate membership

Ensure the entity is valid for the project

Tag Naming Rules

All tags must follow naming conventions:
def validate
  validates_format(
    %r{\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z}i,
    :name,
    message: "must only include ASCII letters, numbers, and dashes, and must start and end with an ASCII letter or number"
  )
  super
end
Valid names:
  • admin
  • read-only
  • team-1
Invalid names:
  • -admin (starts with dash)
  • team_1 (contains underscore)
  • a * 64 (too long)

Tag Cleanup

When a tag is destroyed, all related entries are cleaned up:
def before_destroy
  meta_cond = {object_id: respond_to?(:metatag_uuid) ? metatag_uuid : id}
  applied_dataset.where(tag_id: id).or(applied_column => id).delete
  DB[:applied_object_tag].where(meta_cond).delete
  AccessControlEntry.where(applied_column => id).or(meta_cond).destroy
  super
end
This ensures:
  • Applied tag entries are removed
  • Object tag references are cleaned up
  • Access control entries are destroyed

API Key Integration

API keys can be used as subjects in ABAC:

Unrestricted Access

def unrestrict_token_for_project(project_id)
  AccessControlEntry.where(project_id: project_id, subject_id: id).destroy
  DB[:applied_subject_tag]
    .insert_ignore
    .insert(subject_id: id, tag_id: SubjectTag.where(project_id: project_id, name: "Admin").select(:id))
end
This adds the API key to the Admin subject tag, granting full access.

Restricted Access

def restrict_token_for_project(project_id)
  unrestricted_project_access_dataset(project_id).delete
end

def unrestricted_token_for_project?(project_id)
  !unrestricted_project_access_dataset(project_id).empty?
end

private

def unrestricted_project_access_dataset(project_id)
  DB[:applied_subject_tag]
    .where(subject_id: id, tag_id: SubjectTag.where(project_id: project_id, name: "Admin").select(:id))
end

Example Use Cases

Read-Only Access

Create a read-only role for a team:
# Create subject tag for the team
team_tag = SubjectTag.create(project_id: project.id, name: "read-only-team")
team_tag.add_members([user1.id, user2.id, api_key.id])

# Create action tag for read operations
read_tag = ActionTag.create(project_id: project.id, name: "read-ops")
read_tag.add_members([view_action.id, list_action.id])

# Grant access to all VMs
vm_tag = ObjectTag.create(project_id: project.id, name: "all-vms")
vm_tag.add_members(project.vms.map(&:id))

AccessControlEntry.create(
  project_id: project.id,
  subject_id: team_tag.id,
  action_id: read_tag.id,
  object_id: vm_tag.id
)

Inference Endpoint Access

Grant an API key access to specific inference endpoints:
# Create object tag for production endpoints
prod_endpoints = ObjectTag.create(project_id: project.id, name: "production-endpoints")
prod_endpoints.add_members([endpoint1.id, endpoint2.id])

# Create subject for the API key
api_key_tag = SubjectTag.create(project_id: project.id, name: "inference-api-keys")
api_key_tag.add_subject(api_key.id)

# Grant view and use permissions
AccessControlEntry.create(
  project_id: project.id,
  subject_id: api_key_tag.id,
  action_id: use_action.id,
  object_id: prod_endpoints.id
)

Nested Teams

Create hierarchical team structures:
# Create parent team
engineering = SubjectTag.create(project_id: project.id, name: "engineering")

# Create child teams
frontend = SubjectTag.create(project_id: project.id, name: "frontend-team")
backend = SubjectTag.create(project_id: project.id, name: "backend-team")

# Add child teams to parent
engineering.add_members([frontend.id, backend.id])

# Add users to child teams
frontend.add_members([user1.id, user2.id])
backend.add_members([user3.id, user4.id])

# Grant permissions to parent team (inherited by children)
AccessControlEntry.create(
  project_id: project.id,
  subject_id: engineering.id,
  action_id: nil,  # All actions
  object_id: project.id
)

Resource Paths

Tags have consistent URL paths:
def path
  "/user/access-control/tag/#{base}/#{ubid}"
end
Examples:
  • Subject tag: /user/access-control/tag/subject/st8abc123def456
  • Action tag: /user/access-control/tag/action/at8xyz789ghi012
  • Object tag: /user/access-control/tag/object/ot8uvw345jkl678

Best Practices

Use Tags for Grouping

Instead of creating ACEs for individual users, group them into subject tags:
# Good
team_tag.add_members([user1.id, user2.id, user3.id])
AccessControlEntry.create(subject_id: team_tag.id, ...)

# Avoid
AccessControlEntry.create(subject_id: user1.id, ...)
AccessControlEntry.create(subject_id: user2.id, ...)
AccessControlEntry.create(subject_id: user3.id, ...)

Principle of Least Privilege

Grant only the minimum necessary permissions:
# Good - specific action
AccessControlEntry.create(
  subject_id: subject_tag.id,
  action_id: view_action.id,
  object_id: object_tag.id
)

# Avoid - wildcard action unless necessary
AccessControlEntry.create(
  subject_id: subject_tag.id,
  action_id: nil,
  object_id: object_tag.id
)

Use Object Tags for Environments

Organize resources by environment:
production = ObjectTag.create(project_id: project.id, name: "production")
staging = ObjectTag.create(project_id: project.id, name: "staging")
development = ObjectTag.create(project_id: project.id, name: "development")

production.add_members([prod_vm1.id, prod_vm2.id, prod_endpoint.id])
staging.add_members([staging_vm1.id, staging_endpoint.id])

Build docs developers (and LLMs) love