Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 6 additions & 29 deletions taco/cmd/statesman/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,55 +109,32 @@ func main() {
log.Printf("Query backend already has %d units, skipping sync", len(existingUnits))
}

// create repository
// repository coordinates blob storage with query index internally
// Get the underlying *gorm.DB from the query store
db := repositories.GetDBFromQueryStore(queryStore)
if db == nil {
log.Fatalf("Query store does not provide GetDB method")
}

// Ensure default organization exists
defaultOrgUUID, err := repositories.EnsureDefaultOrganization(context.Background(), db)
if err != nil {
log.Fatalf("Failed to ensure default organization: %v", err)
}
log.Printf("Default organization ensured: %s", defaultOrgUUID)

repo := repositories.NewUnitRepository(db, blobStore)
log.Println("Repository initialized (database-first with blob storage backend)")

// Create RBAC Manager
rbacManager, err := rbac.NewRBACManagerFromQueryStore(queryStore)
if err != nil {
log.Fatalf("Failed to create RBAC manager: %v", err)
}

// --- Create Domain Interfaces with Optional Authorization ---
// These interfaces are what handlers will use
var fullRepo domain.UnitRepository = repo

// Wrap with authorization if auth is enabled
if !*authDisable {
log.Println("Authorization is ENABLED. Wrapping repository with RBAC.")

// Create bootstrap context with default org for RBAC check
// During startup, we need org context to check RBAC status
bootstrapCtx := domain.ContextWithOrg(context.Background(), defaultOrgUUID)

// Verify RBAC manager was created successfully (fail closed for security)
canInit, err := rbacManager.IsEnabled(bootstrapCtx)
if err != nil {
log.Fatalf("Failed to verify RBAC manager: %v", err)
}

if !canInit {
log.Println("RBAC is NOT initialized. System will operate in permissive mode until RBAC is initialized via /v1/rbac/init")
}

fullRepo = repositories.NewAuthorizingRepository(repo, rbacManager)
} else {
log.Println("Authorization is DISABLED via flag. All operations allowed.")

// Ensure system org exists for auth-disabled mode
if err := repositories.EnsureSystemOrganization(context.Background(), db, domain.SystemOrgUUID); err != nil {
log.Fatalf("Failed to create system organization: %v", err)
}
log.Printf("System organization ensured: %s (name: system)", domain.SystemOrgUUID)
}

// Initialize analytics with system ID management (always create system ID)
Expand Down
110 changes: 85 additions & 25 deletions taco/cmd/taco/commands/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ func init() {

// rbac user assign command
var rbacUserAssignCmd = &cobra.Command{
Use: "assign <email> <role-id>",
Use: "assign <email> <role-name>",
Short: "Assign a role to a user",
Long: `Assign a role to a user by email address. The user must have logged in at least once to be found in the system.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()

email := args[0]
roleID := args[1]
roleID := mustResolveRoleID(context.Background(), client, args[1])

printVerbose("Assigning role %s to user %s", roleID, email)

Expand All @@ -209,15 +209,15 @@ var rbacUserAssignCmd = &cobra.Command{

// rbac user revoke command
var rbacUserRevokeCmd = &cobra.Command{
Use: "revoke <email> <role-id>",
Use: "revoke <email> <role-name>",
Short: "Revoke a role from a user",
Long: `Revoke a role from a user by email address.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()

email := args[0]
roleID := args[1]
roleID := mustResolveRoleID(context.Background(), client, args[1])

printVerbose("Revoking role %s from user %s", roleID, email)

Expand Down Expand Up @@ -382,13 +382,14 @@ var rbacRoleListCmd = &cobra.Command{

// Create tabwriter
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tDESCRIPTION\tPERMISSIONS\tCREATED")
fmt.Fprintln(w, "NAME\tDESCRIPTION\tPERMISSIONS\tCREATED")

for _, role := range roles {
permissions := strings.Join(role.Permissions, ", ")
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
role.ID,
role.Name,
name := role.Name
if name == "" { name = role.ID }
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
name,
role.Description,
permissions,
role.CreatedAt,
Expand All @@ -404,14 +405,14 @@ var rbacRoleListCmd = &cobra.Command{

// rbac role delete command
var rbacRoleDeleteCmd = &cobra.Command{
Use: "delete <role-id>",
Use: "delete <role-name>",
Short: "Delete a role",
Long: `Delete a role by ID.`,
Long: `Delete a role by name.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()

roleID := args[0]
roleID := mustResolveRoleID(context.Background(), client, args[0])

printVerbose("Deleting role %s", roleID)

Expand Down Expand Up @@ -597,7 +598,7 @@ var rbacPermissionListCmd = &cobra.Command{
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tName\tDescription\tRules\tCreated")
fmt.Fprintln(w, "NAME\tDESCRIPTION\tRULES\tCREATED")

for _, permission := range permissions {
rules := ""
Expand All @@ -608,9 +609,10 @@ var rbacPermissionListCmd = &cobra.Command{
rules += fmt.Sprintf("%s:%s:%s", rule.Effect, strings.Join(rule.Actions, ","), strings.Join(rule.Resources, ","))
}

fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
permission.ID,
permission.Name,
name := permission.Name
if name == "" { name = permission.ID }
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
name,
permission.Description,
rules,
permission.CreatedAt,
Expand All @@ -625,11 +627,11 @@ var rbacPermissionListCmd = &cobra.Command{

// rbac permission delete command
var rbacPermissionDeleteCmd = &cobra.Command{
Use: "delete <id>",
Use: "delete <name>",
Short: "Delete a permission",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id := args[0]
id := mustResolvePermissionID(context.Background(), newAuthedClient(), args[0])

client := newAuthedClient()

Expand Down Expand Up @@ -895,15 +897,14 @@ func testUserListOutput(client *sdk.Client, email string, args []string) (*TestR

// rbac role assign-policy command
var rbacRoleAssignPolicyCmd = &cobra.Command{
Use: "assign-policy <role-id> <policy-id>",
Use: "assign-policy <role-name> <permission-name>",
Short: "Assign a policy to a role",
Long: `Assign a policy to a role, giving the role the permissions defined in the policy.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
roleID := args[0]
permissionID := args[1]

client := newAuthedClient()
roleID := mustResolveRoleID(context.Background(), client, args[0])
permissionID := mustResolvePermissionID(context.Background(), client, args[1])

req := map[string]string{
"role_id": roleID,
Expand All @@ -926,15 +927,14 @@ var rbacRoleAssignPolicyCmd = &cobra.Command{

// rbac role revoke-permission command
var rbacRoleRevokePermissionCmd = &cobra.Command{
Use: "revoke-permission <role-id> <permission-id>",
Use: "revoke-permission <role-name> <permission-name>",
Short: "Revoke a permission from a role",
Long: `Revoke a permission from a role, removing the access rights defined in the permission.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
roleID := args[0]
permissionID := args[1]

client := newAuthedClient()
roleID := mustResolveRoleID(context.Background(), client, args[0])
permissionID := mustResolvePermissionID(context.Background(), client, args[1])

resp, err := client.Delete(context.Background(), "/v1/rbac/roles/"+roleID+"/permissions/"+permissionID)
if err != nil {
Expand All @@ -949,3 +949,63 @@ var rbacRoleRevokePermissionCmd = &cobra.Command{
return nil
},
}

// mustResolveRoleID resolves a role name to its ID
// If the argument is already a valid identifier, it's returned as-is
func mustResolveRoleID(ctx context.Context, client *sdk.Client, arg string) string {
resp, err := client.Get(ctx, "/v1/rbac/roles")
if err != nil || resp.StatusCode != 200 {
return arg // fallback
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return arg
}

var roles []Role
if err := json.Unmarshal(body, &roles); err != nil {
return arg
}

for _, r := range roles {
if r.Name == arg || r.ID == arg {
if r.ID != "" {
return r.ID
}
return arg
}
}
return arg
}

// mustResolvePermissionID resolves a permission name to its ID
// If the argument is already a valid identifier, it's returned as-is
func mustResolvePermissionID(ctx context.Context, client *sdk.Client, arg string) string {
resp, err := client.Get(ctx, "/v1/rbac/permissions")
if err != nil || resp.StatusCode != 200 {
return arg // fallback
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return arg
}

var permissions []Permission
if err := json.Unmarshal(body, &permissions); err != nil {
return arg
}

for _, p := range permissions {
if p.Name == arg || p.ID == arg {
if p.ID != "" {
return p.ID
}
return arg
}
}
return arg
}
47 changes: 37 additions & 10 deletions taco/cmd/taco/commands/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ func init() {
unitCmd.AddCommand(unitStatusCmd)
}

// mustResolveUnitID resolves a unit name to its ID within the current org using the API.
// If the argument already looks like an ID (UUID or contains '/'), it is returned as-is.
func mustResolveUnitID(ctx context.Context, client *sdk.Client, arg string) string {
// Pass through hierarchical names like prefix/name
if strings.Contains(arg, "/") {
return arg
}
// Fast path: if this looks like a UUID, treat as ID
if _, err := uuid.Parse(arg); err == nil {
return arg
}
// Resolve by listing with prefix and exact match on Name
resp, err := client.ListUnits(ctx, "")
if err != nil || len(resp.Units) == 0 {
return arg // fallback to original arg
}
for _, u := range resp.Units {
if u.Name == arg || u.ID == arg {
if u.ID != "" { return u.ID }
return arg
}
}
return arg
}

var unitCreateCmd = &cobra.Command{
Use: "create <unit-id>",
Short: "Create a new unit",
Expand Down Expand Up @@ -173,11 +198,13 @@ var unitListCmd = &cobra.Command{
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tSIZE\tUPDATED\tLOCKED")
fmt.Fprintln(w, "NAME\tSIZE\tUPDATED\tLOCKED")
for _, u := range filtered {
locked := ""
if u.Locked { locked = "yes" }
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", u.ID, u.Size, u.Updated.Format("2006-01-02 15:04:05"), locked)
name := u.Name
if name == "" { name = u.ID }
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", name, u.Size, u.Updated.Format("2006-01-02 15:04:05"), locked)
}
w.Flush()
fmt.Printf("\nTotal: %d units (showing %d with read access)\n", resp.Count, len(filtered))
Expand All @@ -192,7 +219,7 @@ var unitInfoCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
printVerbose("Getting unit metadata: %s", unitID)
unit, err := client.GetUnit(context.Background(), unitID)
if err != nil { return fmt.Errorf("failed to get unit info: %w", err) }
Expand All @@ -209,7 +236,7 @@ var unitDeleteCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
printVerbose("Deleting unit: %s", unitID)
if err := client.DeleteUnit(context.Background(), unitID); err != nil {
return fmt.Errorf("failed to delete unit: %w", err)
Expand All @@ -227,7 +254,7 @@ var unitPullCmd = &cobra.Command{
analytics.SendEssential("taco_unit_pull_started")

client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
printVerbose("Downloading unit: %s", unitID)
data, err := client.DownloadUnit(context.Background(), unitID)
if err != nil {
Expand Down Expand Up @@ -257,7 +284,7 @@ var unitPushCmd = &cobra.Command{
analytics.SendEssential("taco_unit_push_started")

client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
inputFile := args[1]
printVerbose("Uploading unit: %s from %s", unitID, inputFile)
data, err := os.ReadFile(inputFile)
Expand All @@ -282,7 +309,7 @@ var unitLockCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
printVerbose("Locking unit: %s", unitID)
lockInfo := &sdk.LockInfo{ID: uuid.New().String(), Who: fmt.Sprintf("taco@%s", getHostname()), Version: "1.0.0", Created: time.Now()}
result, err := client.LockUnit(context.Background(), unitID, lockInfo)
Expand All @@ -299,7 +326,7 @@ var unitUnlockCmd = &cobra.Command{
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
client := newAuthedClient()
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
lockID := ""
if len(args) > 1 { lockID = args[1] } else { lockID = getLockID(unitID); if lockID == "" { return fmt.Errorf("no lock ID provided and none found for %s", unitID) } }
printVerbose("Unlocking unit: %s with lock ID: %s", unitID, lockID)
Expand All @@ -316,7 +343,7 @@ var unitAcquireCmd = &cobra.Command{
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
client := sdk.NewClient(serverURL)
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
printVerbose("Acquiring unit: %s", unitID)
lockInfo := &sdk.LockInfo{ID: uuid.New().String(), Who: fmt.Sprintf("taco@%s", getHostname()), Version: "1.0.0", Created: time.Now()}
result, err := client.LockUnit(context.Background(), unitID, lockInfo)
Expand Down Expand Up @@ -346,7 +373,7 @@ var unitReleaseCmd = &cobra.Command{
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client := sdk.NewClient(serverURL)
unitID := args[0]
unitID := mustResolveUnitID(context.Background(), client, args[0])
inputFile := args[1]
printVerbose("Releasing unit: %s", unitID)
lockID := getLockID(unitID)
Expand Down
Loading
Loading