Add mobile_device.create_enroll_token permission; make boombuler/barcode a direct dep (#67863)

* Add `mobile_device` kind for `mobile_device.create_enroll_token` permission

* Update preset-roles.json

* Add `mobile_device.create_enroll_token` to user ACL

* Make boombuler/barcode a direct dep

It's used in teleport.e to generate the QR code that's going to be
consumed by the mobile app.

* Enable defaultAllowRules to return roles which need `buildType`

* Backfill device-admin role

* Add `mobile_device.create_enroll_token` to admin role
This commit is contained in:
Maja Cieślak
2026-06-26 15:53:20 +00:00
committed by GitHub
parent 2169b08d22
commit ed40e1c66d
16 changed files with 137 additions and 22 deletions
+6
View File
@@ -507,6 +507,12 @@ const (
// KindDevice represents a registered or trusted device. // KindDevice represents a registered or trusted device.
KindDevice = "device" KindDevice = "device"
// KindMobileDevice is used to gate access to the mobile device enrollment
// ceremony. It is not a stored resource, it exists only as the target of RBAC
// rules. The verb create_enroll_token permits a user to initiate enrollment
// of a mobile device.
KindMobileDevice = "mobile_device"
// KindEnrollPairing is the resource kind for a mobile device enrollment // KindEnrollPairing is the resource kind for a mobile device enrollment
// pairing: a short-lived ceremony that pairs a Web UI session with the // pairing: a short-lived ceremony that pairs a Web UI session with the
// Teleport Verify mobile app via a QR-code-encoded token. // Teleport Verify mobile app via a QR-code-encoded token.
+2
View File
@@ -79,6 +79,8 @@ import (
_ "github.com/aws/smithy-go" _ "github.com/aws/smithy-go"
_ "github.com/aws/smithy-go/transport/http" _ "github.com/aws/smithy-go/transport/http"
_ "github.com/beevik/etree" _ "github.com/beevik/etree"
_ "github.com/boombuler/barcode"
_ "github.com/boombuler/barcode/qr"
_ "github.com/coreos/go-semver/semver" _ "github.com/coreos/go-semver/semver"
_ "github.com/crewjam/saml" _ "github.com/crewjam/saml"
_ "github.com/crewjam/saml/samlsp" _ "github.com/crewjam/saml/samlsp"
+8
View File
@@ -649,6 +649,14 @@
"enroll" "enroll"
] ]
}, },
{
"resources": [
"mobile_device"
],
"verbs": [
"create_enroll_token"
]
},
{ {
"resources": [ "resources": [
"db_service" "db_service"
+1 -1
View File
@@ -101,6 +101,7 @@ require (
github.com/aws/smithy-go/tracing/smithyoteltracing v1.0.9 github.com/aws/smithy-go/tracing/smithyoteltracing v1.0.9
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0
github.com/beevik/etree v1.6.0 github.com/beevik/etree v1.6.0
github.com/boombuler/barcode v1.0.1
github.com/buildkite/bintest/v3 v3.3.0 github.com/buildkite/bintest/v3 v3.3.0
github.com/charlievieth/strcase v0.0.5 github.com/charlievieth/strcase v0.0.5
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
@@ -343,7 +344,6 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+1
View File
@@ -1329,6 +1329,7 @@ func unscopedDefinitionForBuiltinRole(clusterName string, recConfig readonly.Ses
Rules: []types.Rule{ Rules: []types.Rule{
types.NewRule(types.Wildcard, services.RW()), types.NewRule(types.Wildcard, services.RW()),
types.NewRule(types.KindDevice, append(services.RW(), types.VerbCreateEnrollToken, types.VerbEnroll)), types.NewRule(types.KindDevice, append(services.RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
types.NewRule(types.KindMobileDevice, []string{types.VerbCreateEnrollToken}),
}, },
}, },
}) })
+23 -13
View File
@@ -180,6 +180,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindDatabaseCertificate, RW()), types.NewRule(types.KindDatabaseCertificate, RW()),
types.NewRule(types.KindInstaller, RW()), types.NewRule(types.KindInstaller, RW()),
types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)), types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
types.NewRule(types.KindMobileDevice, []string{types.VerbCreateEnrollToken}),
types.NewRule(types.KindDatabaseService, RO()), types.NewRule(types.KindDatabaseService, RO()),
types.NewRule(types.KindInstance, RO()), types.NewRule(types.KindInstance, RO()),
types.NewRule(types.KindLoginRule, RW()), types.NewRule(types.KindLoginRule, RW()),
@@ -492,6 +493,7 @@ func NewPresetDeviceAdminRole(buildType string) types.Role {
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: []types.Rule{ Rules: []types.Rule{
types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)), types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
types.NewRule(types.KindMobileDevice, []string{types.VerbCreateEnrollToken}),
}, },
}, },
}, },
@@ -1132,23 +1134,31 @@ func bootstrapRoleMetadataLabels() map[string]map[string]string {
} }
} }
var defaultAllowRulesMap = map[string][]types.Rule{
teleport.PresetAuditorRoleName: NewPresetAuditorRole().GetRules(types.Allow),
teleport.PresetEditorRoleName: NewPresetEditorRole().GetRules(types.Allow),
teleport.PresetAccessRoleName: NewPresetAccessRole().GetRules(types.Allow),
teleport.PresetTerraformProviderRoleName: NewPresetTerraformProviderRole().GetRules(types.Allow),
teleport.PresetAccessPluginRoleName: NewPresetAccessPluginRole().GetRules(types.Allow),
teleport.PresetAccessPluginWithReviewRoleName: NewPresetAccessPluginWithReviewRole().GetRules(types.Allow),
teleport.PresetListAccessRequestResourcesRoleName: NewPresetListAccessRequestResourcesRole().GetRules(types.Allow),
}
// defaultAllowRules has the Allow rules that should be set as default when // defaultAllowRules has the Allow rules that should be set as default when
// they were not explicitly defined. This is used to update the current cluster // they were not explicitly defined. This is used to update the current cluster
// roles when deploying a new resource. It will also update all existing roles // roles when deploying a new resource. It will also update all existing roles
// on auth server restart. Rules defined in preset template should be // on auth server restart. Rules defined in preset template should be
// exactly the same rule when added here. // exactly the same rule when added here.
func defaultAllowRules() map[string][]types.Rule { func defaultAllowRules(buildType string) map[string][]types.Rule {
return defaultAllowRulesMap roles := []types.Role{
NewPresetAuditorRole(),
NewPresetEditorRole(),
NewPresetAccessRole(),
NewPresetTerraformProviderRole(),
NewPresetAccessPluginRole(),
NewPresetAccessPluginWithReviewRole(),
NewPresetListAccessRequestResourcesRole(),
NewPresetDeviceAdminRole(buildType),
}
allowRules := make(map[string][]types.Rule, len(roles))
for _, role := range roles {
if role == nil {
continue
}
allowRules[role.GetName()] = role.GetRules(types.Allow)
}
return allowRules
} }
// defaultAllowLabels has the Allow labels that should be set as default when they were not explicitly defined. // defaultAllowLabels has the Allow labels that should be set as default when they were not explicitly defined.
@@ -1299,7 +1309,7 @@ func AddRoleDefaults(ctx context.Context, buildType string, role types.Role) (ty
} }
// Resource Rules // Resource Rules
defaultRules, ok := defaultAllowRules()[role.GetName()] defaultRules, ok := defaultAllowRules(buildType)[role.GetName()]
if ok { if ok {
existingRules := append(role.GetRules(types.Allow), role.GetRules(types.Deny)...) existingRules := append(role.GetRules(types.Allow), role.GetRules(types.Deny)...)
+52 -8
View File
@@ -99,7 +99,7 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
Spec: types.RoleSpecV6{ Spec: types.RoleSpecV6{
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: defaultAllowRules()[teleport.PresetEditorRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetEditorRoleName],
}, },
}, },
}, },
@@ -113,7 +113,7 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
Spec: types.RoleSpecV6{ Spec: types.RoleSpecV6{
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: defaultAllowRules()[teleport.PresetEditorRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetEditorRoleName],
}, },
}, },
}, },
@@ -162,7 +162,7 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
Spec: types.RoleSpecV6{ Spec: types.RoleSpecV6{
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: defaultAllowRules()[teleport.PresetAccessRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
}, },
}, },
}, },
@@ -178,7 +178,7 @@ func TestAddRoleDefaults(t *testing.T) {
Allow: types.RoleConditions{ Allow: types.RoleConditions{
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels, DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles, DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
Rules: defaultAllowRules()[teleport.PresetAccessRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
GitHubPermissions: []types.GitHubPermission{{ GitHubPermissions: []types.GitHubPermission{{
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName], Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
}}, }},
@@ -199,7 +199,7 @@ func TestAddRoleDefaults(t *testing.T) {
Allow: types.RoleConditions{ Allow: types.RoleConditions{
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels, DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles, DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
Rules: defaultAllowRules()[teleport.PresetAccessRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
GitHubPermissions: []types.GitHubPermission{{ GitHubPermissions: []types.GitHubPermission{{
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName], Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
}}, }},
@@ -221,7 +221,7 @@ func TestAddRoleDefaults(t *testing.T) {
Allow: types.RoleConditions{ Allow: types.RoleConditions{
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels, DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles, DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
Rules: defaultAllowRules()[teleport.PresetAccessRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
GitHubPermissions: []types.GitHubPermission{{ GitHubPermissions: []types.GitHubPermission{{
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName], Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
}}, }},
@@ -288,7 +288,7 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
}, },
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: defaultAllowRules()[teleport.PresetAuditorRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAuditorRoleName],
}, },
}, },
}, },
@@ -309,7 +309,7 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
}, },
Allow: types.RoleConditions{ Allow: types.RoleConditions{
Rules: defaultAllowRules()[teleport.PresetAuditorRoleName], Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAuditorRoleName],
}, },
}, },
}, },
@@ -901,6 +901,50 @@ func TestAddRoleDefaults(t *testing.T) {
}, },
}, },
}, },
{
name: "device-admin (missing mobile_device.create_enroll_token)",
enterprise: true,
role: &types.RoleV6{
Kind: types.KindRole,
Version: types.V8,
Metadata: types.Metadata{
Name: teleport.PresetDeviceAdminRoleName,
Namespace: apidefaults.Namespace,
Description: "Administer trusted devices",
Labels: map[string]string{
types.TeleportInternalResourceType: types.PresetResource,
},
},
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
},
},
},
},
expectedErr: require.NoError,
expected: &types.RoleV6{
Kind: types.KindRole,
Version: types.V8,
Metadata: types.Metadata{
Name: teleport.PresetDeviceAdminRoleName,
Namespace: apidefaults.Namespace,
Description: "Administer trusted devices",
Labels: map[string]string{
types.TeleportInternalResourceType: types.PresetResource,
},
},
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindDevice, append(RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
types.NewRule(types.KindMobileDevice, []string{types.VerbCreateEnrollToken}),
},
},
},
},
},
} }
for _, test := range tests { for _, test := range tests {
+16
View File
@@ -35,6 +35,15 @@ type ResourceAccess struct {
Use bool `json:"use"` Use bool `json:"use"`
} }
// MobileDeviceAccess defines permissions for the mobile_device resource.
// It uses a dedicated shape rather than ResourceAccess because mobile_device
// exposes a custom verb, not the standard list/read/edit/create/delete/use set.
type MobileDeviceAccess struct {
// CreateEnrollToken reflects the mobile_device.create_enroll_token verb,
// which gates a user's ability to start mobile device enrollment.
CreateEnrollToken bool `json:"createEnrollToken"`
}
// UserACL is derived from a user's role set and includes // UserACL is derived from a user's role set and includes
// information as to what features the user is allowed to use. // information as to what features the user is allowed to use.
type UserACL struct { type UserACL struct {
@@ -150,6 +159,8 @@ type UserACL struct {
AutoUpdateAgentReport ResourceAccess `json:"autoUpdateAgentReport"` AutoUpdateAgentReport ResourceAccess `json:"autoUpdateAgentReport"`
// Beam defines access to Beams // Beam defines access to Beams
Beam ResourceAccess `json:"beam"` Beam ResourceAccess `json:"beam"`
// MobileDevice defines permissions for the mobile_device resource.
MobileDevice MobileDeviceAccess `json:"mobileDevice"`
} }
func hasAccess(roleSet RoleSet, ctx *Context, kind string, verbs ...string) bool { func hasAccess(roleSet RoleSet, ctx *Context, kind string, verbs ...string) bool {
@@ -273,6 +284,10 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des
beam = newAccess(userRoles, ctx, types.KindBeam) beam = newAccess(userRoles, ctx, types.KindBeam)
} }
mobileDevice := MobileDeviceAccess{
CreateEnrollToken: hasAccess(userRoles, ctx, types.KindMobileDevice, types.VerbCreateEnrollToken),
}
return UserACL{ return UserACL{
AccessRequests: requestAccess, AccessRequests: requestAccess,
AppServers: appServerAccess, AppServers: appServerAccess,
@@ -330,5 +345,6 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des
AutoUpdateAgentRollout: autoUpdateAgentRollout, AutoUpdateAgentRollout: autoUpdateAgentRollout,
AutoUpdateAgentReport: autoUpdateAgentReport, AutoUpdateAgentReport: autoUpdateAgentReport,
Beam: beam, Beam: beam,
MobileDevice: mobileDevice,
} }
} }
+6
View File
@@ -76,6 +76,10 @@ func TestNewUserACL(t *testing.T) {
Resources: []string{types.KindBeam}, Resources: []string{types.KindBeam},
Verbs: RW(), Verbs: RW(),
}, },
{
Resources: []string{types.KindMobileDevice},
Verbs: []string{types.VerbCreateEnrollToken},
},
}) })
// not setting the rule, or explicitly denying, both denies Access // not setting the rule, or explicitly denying, both denies Access
@@ -142,6 +146,8 @@ func TestNewUserACL(t *testing.T) {
// beams should be denied without the Beams entitlement // beams should be denied without the Beams entitlement
require.Empty(t, cmp.Diff(userContext.Beam, denied)) require.Empty(t, cmp.Diff(userContext.Beam, denied))
require.True(t, userContext.MobileDevice.CreateEnrollToken)
// test enabling of the 'Use' verb // test enabling of the 'Use' verb
require.Empty(t, cmp.Diff(userContext.Integrations, ResourceAccess{true, true, true, true, true, true})) require.Empty(t, cmp.Diff(userContext.Integrations, ResourceAccess{true, true, true, true, true, true}))
@@ -1308,6 +1308,7 @@ const additionalVerbs = new Map<string, Verb[]>([
[ResourceKind.GithubConnector, ['readnosecrets']], [ResourceKind.GithubConnector, ['readnosecrets']],
[ResourceKind.Semaphore, ['readnosecrets']], [ResourceKind.Semaphore, ['readnosecrets']],
[ResourceKind.Device, ['create_enroll_token', 'enroll']], [ResourceKind.Device, ['create_enroll_token', 'enroll']],
[ResourceKind.MobileDevice, ['create_enroll_token']],
[ResourceKind.AuditQuery, ['use']], [ResourceKind.AuditQuery, ['use']],
[ResourceKind.SecurityReport, ['use']], [ResourceKind.SecurityReport, ['use']],
[ResourceKind.Integration, ['use']], [ResourceKind.Integration, ['use']],
@@ -91,6 +91,7 @@ export const allAccessAcl: Acl = {
inferenceModel: fullAccess, inferenceModel: fullAccess,
inferenceSecret: fullAccess, inferenceSecret: fullAccess,
beam: fullAccess, beam: fullAccess,
mobileDevice: { createEnrollToken: true },
}; };
export function getAcl(cfg?: { noAccess: boolean }) { export function getAcl(cfg?: { noAccess: boolean }) {
@@ -313,6 +313,7 @@ export enum ResourceKind {
Lock = 'lock', Lock = 'lock',
LoginRule = 'login_rule', LoginRule = 'login_rule',
MFADevice = 'mfa_device', MFADevice = 'mfa_device',
MobileDevice = 'mobile_device',
// Ignoring duplicate: KindNamespace = "namespace" // Ignoring duplicate: KindNamespace = "namespace"
NetworkRestrictions = 'network_restrictions', NetworkRestrictions = 'network_restrictions',
Node = 'node', Node = 'node',
@@ -105,6 +105,8 @@ export function makeAcl(json): Acl {
const beam = json.beam || defaultAccess; const beam = json.beam || defaultAccess;
const mobileDevice = json.mobileDevice || defaultMobileDeviceAccess;
return { return {
accessList, accessList,
authConnectors, authConnectors,
@@ -159,6 +161,7 @@ export function makeAcl(json): Acl {
inferenceModel, inferenceModel,
inferenceSecret, inferenceSecret,
beam, beam,
mobileDevice,
}; };
} }
@@ -174,3 +177,7 @@ export const defaultAccessWithUse = {
...defaultAccess, ...defaultAccess,
use: false, use: false,
}; };
export const defaultMobileDeviceAccess = {
createEnrollToken: false,
};
@@ -73,6 +73,10 @@ export interface AccessWithUse extends Access {
use: boolean; use: boolean;
} }
export interface MobileDeviceAccess {
createEnrollToken: boolean;
}
export interface Acl { export interface Acl {
directorySharingEnabled: boolean; directorySharingEnabled: boolean;
reviewRequests: boolean; reviewRequests: boolean;
@@ -130,6 +134,7 @@ export interface Acl {
inferenceModel: Access; inferenceModel: Access;
inferenceSecret: Access; inferenceSecret: Access;
beam: Access; beam: Access;
mobileDevice: MobileDeviceAccess;
} }
// AllTraits represent all the traits defined for a user. // AllTraits represent all the traits defined for a user.
@@ -388,6 +388,9 @@ test('undefined values in context response gives proper default values', async (
create: false, create: false,
remove: false, remove: false,
}, },
mobileDevice: {
createEnrollToken: false,
},
}; };
expect(response).toEqual({ expect(response).toEqual({
@@ -322,4 +322,8 @@ export default class StoreUserContext extends Store<UserContext> {
getBeamAccess() { getBeamAccess() {
return this.state.acl.beam; return this.state.acl.beam;
} }
getMobileDeviceAccess() {
return this.state.acl.mobileDevice;
}
} }