Ctrl K
ComunidadGuías…

pkg/auth/cloud_role.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package auth

import (
	"context"
	"strings"
)

// CloudRole captures the user's tier under Radar Cloud's RBAC model.
// Cloud injects one of `radar:owner` / `radar:member` / `radar:viewer`
// into X-Forwarded-Groups (plus the deprecated `cloud:*` equivalents for
// the transition window); this type extracts that signal.
//
// CloudRole gates Radar's own features — settings, integrations, self-upgrade —
// which run as Radar's service account, so nothing else checks them. It must
// not gate operations that run as the impersonated user (Helm, probe pods):
// Kubernetes RBAC decides those, and when IdP groups grant access the role
// says nothing about what the user may do in the cluster.
type CloudRole string

const (
	// RoleNone means the request is not from a Cloud-attributed user
	// (OSS deploy, or a Cloud bug stripped the role group). AtLeast
	// returns true for RoleNone so non-Cloud deploys aren't gated.
	RoleNone   CloudRole = ""
	RoleViewer CloudRole = "viewer"
	RoleMember CloudRole = "member"
	RoleOwner  CloudRole = "owner"
)

// radar: is the canonical vocabulary; cloud: is the pre-rename legacy still
// emitted by the hub (and possibly present from older hubs) during the
// deprecation window. Accepting both keeps every hub/binary version pairing
// working; drop cloud: in the next major.
var tierGroupPrefixes = []string{"radar:", "cloud:"}

// ErrCodeCloudRoleInsufficient is the stable wire value emitted in 403
// response bodies (`error_code` field) when a request is denied by a
// Cloud role gate. frontend + MCP clients branch on this exact string —
// never rename. Hoisted out of internal/helm/handlers.go so the
// canonical value lives next to the role types it pertains to.
const ErrCodeCloudRoleInsufficient = "cloud_role_insufficient"

var tierRank = map[CloudRole]int{
	RoleViewer: 1,
	RoleMember: 2,
	RoleOwner:  3,
}

// CloudRoleFromGroups returns the highest-ranked Cloud tier present in
// the group list (accepting both the canonical radar:<tier> and legacy
// cloud:<tier> forms). Returns RoleNone when no tier group is found,
// which the call sites treat as "not running under Cloud."
func CloudRoleFromGroups(groups []string) CloudRole {
	var best CloudRole
	bestRank := 0
	for _, g := range groups {
		for _, prefix := range tierGroupPrefixes {
			if !strings.HasPrefix(g, prefix) {
				continue
			}
			role := CloudRole(strings.TrimPrefix(g, prefix))
			rank, ok := tierRank[role]
			if !ok {
				continue
			}
			if rank > bestRank {
				best = role
				bestRank = rank
			}
			break
		}
	}
	return best
}

// CloudRoleFromContext is a convenience over UserFromContext +
// CloudRoleFromGroups. Returns RoleNone when no user is in context.
func CloudRoleFromContext(ctx context.Context) CloudRole {
	user := UserFromContext(ctx)
	if user == nil {
		return RoleNone
	}
	if user.HubGroups != nil {
		return CloudRoleFromGroups(user.HubGroups)
	}
	return CloudRoleFromGroups(user.Groups)
}

// AtLeast reports whether r meets or exceeds min. RoleNone (no Cloud
// role detected) bypasses the gate — non-Cloud deploys must not be
// blocked by Cloud-tier checks.
func (r CloudRole) AtLeast(min CloudRole) bool {
	if r == RoleNone {
		return true
	}
	return tierRank[r] >= tierRank[min]
}

// String returns the role name suitable for log/error messages.
func (r CloudRole) String() string {
	if r == RoleNone {
		return "none"
	}
	return string(r)
}