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
10 changes: 5 additions & 5 deletions pkg/connector/clusterrole.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ func (c *clusterRoleBuilder) List(ctx context.Context, parentResourceID *v2.Reso
func clusterRoleResource(clusterRole *rbacv1.ClusterRole) (*v2.Resource, error) {
// Prepare profile with standard metadata
profile := map[string]interface{}{
"name": clusterRole.Name,
"uid": string(clusterRole.UID),
"creationTimestamp": clusterRole.CreationTimestamp.String(),
"labels": StringMapToAnyMap(clusterRole.Labels),
"annotations": StringMapToAnyMap(clusterRole.Annotations),
metadataKeyName: clusterRole.Name,
metadataKeyUID: string(clusterRole.UID),
metadataKeyCreationTimestamp: clusterRole.CreationTimestamp.String(),
metadataKeyLabels: StringMapToAnyMap(clusterRole.Labels),
metadataKeyAnnotations: StringMapToAnyMap(clusterRole.Annotations),
}

// Add aggregation rule if present
Expand Down
18 changes: 16 additions & 2 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,27 @@ const (
ResourceTypeRoleBinding = "rolebinding"
SubjectTypeGroup = "Group"
SubjectTypeUser = "User"

// Standard Kubernetes object-metadata keys used in resource profiles.
Comment on lines +28 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: SubjectTypeGroup and SubjectTypeUser are now unused — the code was migrated to SubjectKindGroup / SubjectKindUser from helper.go. These can be removed to avoid confusion about which constants to use.

metadataKeyName = "name"
metadataKeyNamespace = "namespace"
metadataKeyUID = "uid"
metadataKeyCreationTimestamp = "creationTimestamp"
metadataKeyLabels = "labels"
metadataKeyAnnotations = "annotations"

// verbGet is the Kubernetes "get" RBAC verb.
verbGet = "get"

// kindRole is the RBAC RoleRef kind for namespaced Roles.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: kindRole semantically represents the RBAC RoleRef.Kind value, but here it's used as a UI DisplayName. These are the same string today ("Role"), but if kindRole were ever changed for RBAC purposes, it would silently change the display name (a breaking change per B1). Consider using the literal "Role" for the display name to keep the two concerns decoupled.

kindRole = "Role"
)

// Resource type definitions.
var (
ResourceTypeNamespace = &v2.ResourceType{Id: "namespace", DisplayName: "Namespace"}
ResourceTypeServiceAccount = &v2.ResourceType{Id: "service_account", DisplayName: "Service Account", Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_USER}}
ResourceTypeRole = &v2.ResourceType{Id: "role", DisplayName: "Role", Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_ROLE}}
ResourceTypeRole = &v2.ResourceType{Id: "role", DisplayName: kindRole, Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_ROLE}}
ResourceTypeClusterRole = &v2.ResourceType{Id: "cluster_role", DisplayName: "Cluster Role", Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_ROLE}}
ResourceTypeSecret = &v2.ResourceType{Id: "secret", DisplayName: "Secret", Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_SECRET}}
ResourceTypeConfigMap = &v2.ResourceType{Id: "configmap", DisplayName: "Config Map"}
Expand Down Expand Up @@ -348,7 +362,7 @@ func (k *Kubernetes) GetMatchingRoleBindings(ctx context.Context, namespace, rol

var result []rbacv1.RoleBinding
for _, binding := range k.roleBindingsCache {
if binding.Namespace == namespace && binding.RoleRef.Kind == "Role" && binding.RoleRef.Name == roleName {
if binding.Namespace == namespace && binding.RoleRef.Kind == kindRole && binding.RoleRef.Name == roleName {
result = append(result, binding)
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/connector/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func generateWildcardResource(resourceType *v2.ResourceType) (*v2.Resource, erro

// Create basic profile data
profile := map[string]interface{}{
"name": displayName,
"uid": "wildcard-" + resourceType.Id,
metadataKeyName: displayName,
metadataKeyUID: "wildcard-" + resourceType.Id,
}

// Handle different resource types differently to add appropriate traits.
Expand Down
10 changes: 5 additions & 5 deletions pkg/connector/kubegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (k *kubeGroupBuilder) List(ctx context.Context, parentResourceID *v2.Resour
// Extract group subjects from bindings
for _, binding := range resp.Items {
for _, subject := range binding.Subjects {
if subject.Kind == "Group" {
if subject.Kind == SubjectKindGroup {
// Process group
k.processGroup(ctx, subject.Name, &rv)
}
Expand All @@ -99,11 +99,11 @@ func (k *kubeGroupBuilder) List(ctx context.Context, parentResourceID *v2.Resour

// Prepare for phase 2
bag = &pagination.Bag{}
bag.Push(pagination.PageState{Token: "clusterrolebindings"})
bag.Push(pagination.PageState{Token: ResourceTypeClusterRoleBindings})
}

// Phase 2: Process ClusterRoleBindings
if pageState == "clusterrolebindings" {
if pageState == ResourceTypeClusterRoleBindings {
// Set up list options with pagination
opts := metav1.ListOptions{
Limit: ResourcesPageSize,
Expand All @@ -120,7 +120,7 @@ func (k *kubeGroupBuilder) List(ctx context.Context, parentResourceID *v2.Resour
// Extract group subjects from bindings
for _, binding := range resp.Items {
for _, subject := range binding.Subjects {
if subject.Kind == "Group" {
if subject.Kind == SubjectKindGroup {
// Process group
k.processGroup(ctx, subject.Name, &rv)
}
Expand Down Expand Up @@ -174,7 +174,7 @@ func (k *kubeGroupBuilder) processGroup(ctx context.Context, groupName string, r
func (k *kubeGroupBuilder) kubeGroupResource(groupName string) (*v2.Resource, error) {
// Create profile
profile := map[string]interface{}{
"name": groupName,
metadataKeyName: groupName,
}

// Create resource with group trait options
Expand Down
10 changes: 5 additions & 5 deletions pkg/connector/kubeuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (k *kubeUserBuilder) List(ctx context.Context, parentResourceID *v2.Resourc
// Extract user subjects from bindings
for _, binding := range resp.Items {
for _, subject := range binding.Subjects {
if subject.Kind == "User" {
if subject.Kind == SubjectKindUser {
// Process user
k.processUser(ctx, subject.Name, &rv)
}
Expand All @@ -89,11 +89,11 @@ func (k *kubeUserBuilder) List(ctx context.Context, parentResourceID *v2.Resourc

// Prepare for phase 2
bag = &pagination.Bag{}
bag.Push(pagination.PageState{Token: "clusterrolebindings"})
bag.Push(pagination.PageState{Token: ResourceTypeClusterRoleBindings})
}

// Phase 2: Process ClusterRoleBindings
if pageState == "clusterrolebindings" {
if pageState == ResourceTypeClusterRoleBindings {
// Set up list options with pagination
opts := metav1.ListOptions{
Limit: ResourcesPageSize,
Expand All @@ -110,7 +110,7 @@ func (k *kubeUserBuilder) List(ctx context.Context, parentResourceID *v2.Resourc
// Extract user subjects from bindings
for _, binding := range resp.Items {
for _, subject := range binding.Subjects {
if subject.Kind == "User" {
if subject.Kind == SubjectKindUser {
// Process user
k.processUser(ctx, subject.Name, &rv)
}
Expand Down Expand Up @@ -164,7 +164,7 @@ func (k *kubeUserBuilder) processUser(ctx context.Context, username string, reso
func (k *kubeUserBuilder) kubeUserResource(username string) (*v2.Resource, error) {
// Create profile
profile := map[string]interface{}{
"name": username,
metadataKeyName: username,
}

// Create resource with user trait options
Expand Down
10 changes: 5 additions & 5 deletions pkg/connector/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ func (n *namespaceBuilder) List(ctx context.Context, parentResourceID *v2.Resour
func namespaceResource(ns *corev1.Namespace) (*v2.Resource, error) {
// Prepare profile with standard metadata
profile := map[string]interface{}{
"name": ns.Name,
"uid": string(ns.UID),
"creationTimestamp": ns.CreationTimestamp.String(),
"labels": StringMapToAnyMap(ns.Labels),
"annotations": StringMapToAnyMap(ns.Annotations),
metadataKeyName: ns.Name,
metadataKeyUID: string(ns.UID),
metadataKeyCreationTimestamp: ns.CreationTimestamp.String(),
metadataKeyLabels: StringMapToAnyMap(ns.Labels),
metadataKeyAnnotations: StringMapToAnyMap(ns.Annotations),
}

// Add status phase if available
Expand Down
12 changes: 6 additions & 6 deletions pkg/connector/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,18 @@ func (r *roleBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId,
func roleResource(role *rbacv1.Role) (*v2.Resource, error) {
// Prepare profile with standard metadata
profile := map[string]interface{}{
"name": role.Name,
"namespace": role.Namespace,
"uid": string(role.UID),
"creationTimestamp": role.CreationTimestamp.String(),
metadataKeyName: role.Name,
metadataKeyNamespace: role.Namespace,
metadataKeyUID: string(role.UID),
metadataKeyCreationTimestamp: role.CreationTimestamp.String(),
}

// Only add labels and annotations if they're not nil to avoid proto conversion issues
if role.Labels != nil {
profile["labels"] = StringMapToAnyMap(role.Labels)
profile[metadataKeyLabels] = StringMapToAnyMap(role.Labels)
}
if role.Annotations != nil {
profile["annotations"] = StringMapToAnyMap(role.Annotations)
profile[metadataKeyAnnotations] = StringMapToAnyMap(role.Annotations)
}

// Get parent namespace resource ID
Expand Down
56 changes: 48 additions & 8 deletions pkg/connector/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package connector
import (
"context"
"fmt"
"strings"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -20,7 +21,7 @@ import (

// Standard verb entitlements for Kubernetes resources.
var standardResourceVerbs = []string{
"get",
verbGet,
"list",
"watch",
"create",
Expand Down Expand Up @@ -110,19 +111,25 @@ func secretResource(secret *corev1.Secret) (*v2.Resource, error) {

// Create profile with standard metadata
profile := map[string]interface{}{
"name": secret.Name,
"namespace": secret.Namespace,
"uid": string(secret.UID),
"creationTimestamp": secret.CreationTimestamp.String(),
"labels": StringMapToAnyMap(secret.Labels),
"annotations": StringMapToAnyMap(secret.Annotations),
"type": string(secret.Type),
metadataKeyName: secret.Name,
metadataKeyNamespace: secret.Namespace,
metadataKeyUID: string(secret.UID),
metadataKeyCreationTimestamp: secret.CreationTimestamp.String(),
metadataKeyLabels: StringMapToAnyMap(secret.Labels),
metadataKeyAnnotations: StringMapToAnyMap(secret.Annotations),
"type": string(secret.Type),
}

// Classify the secret's cryptographic kind onto the NHI spine.
credentialType, credentialDetail := secretCredentialType(secret.Type)

// Secret trait options
secretOptions := []rs.SecretTraitOption{
// Set creation time from metadata
rs.WithSecretCreatedAt(secret.CreationTimestamp.Time),
// NHI spine: cryptographic class + platform-specific detail
rs.WithSecretType(credentialType),
rs.WithSecretDetail(credentialDetail),
// Create a custom trait option for the profile
func(t *v2.SecretTrait) error {
profileStruct, err := structpb.NewStruct(profile)
Expand Down Expand Up @@ -160,6 +167,39 @@ func secretResource(secret *corev1.Secret) (*v2.Resource, error) {
return resource, nil
}

// secretCredentialType maps a Kubernetes secret type onto the NHI spine
// CredentialType and a dotted-lowercase axis-2 detail (e.g.
// "k8s.secret.service_account_token"). TLS secrets carry an x509 certificate
// and SSH-auth secrets carry an asymmetric key pair; everything else (opaque,
// service-account tokens, docker creds, basic-auth, bootstrap tokens) is an
// opaque static secret.
func secretCredentialType(secretType corev1.SecretType) (v2.SecretTrait_CredentialType, string) {
detail := "k8s.secret." + normalizeSecretType(secretType)
switch secretType {
case corev1.SecretTypeTLS:
return v2.SecretTrait_CREDENTIAL_TYPE_CERTIFICATE, detail
case corev1.SecretTypeSSHAuth:
return v2.SecretTrait_CREDENTIAL_TYPE_ASYMMETRIC_KEY, detail
default:
return v2.SecretTrait_CREDENTIAL_TYPE_STATIC_SECRET, detail
}
}

// normalizeSecretType reduces a Kubernetes secret type to a dotted-lowercase
// token: it drops any "<domain>/" prefix, lowercases, and replaces "-" with
// "_" (e.g. "kubernetes.io/service-account-token" -> "service_account_token").
// An empty type defaults to Opaque, matching Kubernetes semantics.
func normalizeSecretType(secretType corev1.SecretType) string {
s := string(secretType)
if s == "" {
s = string(corev1.SecretTypeOpaque)
}
if idx := strings.LastIndex(s, "/"); idx >= 0 {
s = s[idx+1:]
}
return strings.ReplaceAll(strings.ToLower(s), "-", "_")
}

// Entitlements returns standard verb entitlements for Secret resources.
func (s *secretBuilder) Entitlements(ctx context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) {
var entitlements []*v2.Entitlement
Expand Down
65 changes: 65 additions & 0 deletions pkg/connector/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package connector

import (
"testing"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)

// TestSecretCredentialType verifies the Kubernetes secret type -> NHI spine
// CredentialType + axis-2 detail mapping.
func TestSecretCredentialType(t *testing.T) {
testCases := []struct {
name string
secretType corev1.SecretType
wantType v2.SecretTrait_CredentialType
wantDetail string
}{
{
name: "service account token",
secretType: corev1.SecretTypeServiceAccountToken,
wantType: v2.SecretTrait_CREDENTIAL_TYPE_STATIC_SECRET,
wantDetail: "k8s.secret.service_account_token",
},
{
name: "tls is a certificate",
secretType: corev1.SecretTypeTLS,
wantType: v2.SecretTrait_CREDENTIAL_TYPE_CERTIFICATE,
wantDetail: "k8s.secret.tls",
},
{
name: "ssh auth is an asymmetric key",
secretType: corev1.SecretTypeSSHAuth,
wantType: v2.SecretTrait_CREDENTIAL_TYPE_ASYMMETRIC_KEY,
wantDetail: "k8s.secret.ssh_auth",
},
{
name: "opaque is a static secret",
secretType: corev1.SecretTypeOpaque,
wantType: v2.SecretTrait_CREDENTIAL_TYPE_STATIC_SECRET,
wantDetail: "k8s.secret.opaque",
},
{
name: "basic auth is a static secret",
secretType: corev1.SecretTypeBasicAuth,
wantType: v2.SecretTrait_CREDENTIAL_TYPE_STATIC_SECRET,
wantDetail: "k8s.secret.basic_auth",
},
{
name: "empty type defaults to opaque",
secretType: corev1.SecretType(""),
wantType: v2.SecretTrait_CREDENTIAL_TYPE_STATIC_SECRET,
wantDetail: "k8s.secret.opaque",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotType, gotDetail := secretCredentialType(tc.secretType)
assert.Equal(t, tc.wantType, gotType)
assert.Equal(t, tc.wantDetail, gotDetail)
})
}
}
12 changes: 6 additions & 6 deletions pkg/connector/serviceaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ func (s *serviceAccountBuilder) List(ctx context.Context, parentResourceID *v2.R
func serviceAccountResource(serviceAccount *corev1.ServiceAccount) (*v2.Resource, error) {
// Prepare profile with standard metadata
profile := map[string]interface{}{
"name": serviceAccount.Name,
"namespace": serviceAccount.Namespace,
"uid": string(serviceAccount.UID),
"creationTimestamp": serviceAccount.CreationTimestamp.String(),
"labels": StringMapToAnyMap(serviceAccount.Labels),
"annotations": StringMapToAnyMap(serviceAccount.Annotations),
metadataKeyName: serviceAccount.Name,
metadataKeyNamespace: serviceAccount.Namespace,
metadataKeyUID: string(serviceAccount.UID),
metadataKeyCreationTimestamp: serviceAccount.CreationTimestamp.String(),
metadataKeyLabels: StringMapToAnyMap(serviceAccount.Labels),
metadataKeyAnnotations: StringMapToAnyMap(serviceAccount.Annotations),
}

// Add secrets if present
Expand Down
Loading