mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
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:
@@ -507,6 +507,12 @@ const (
|
||||
// KindDevice represents a registered or trusted 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
|
||||
// pairing: a short-lived ceremony that pairs a Web UI session with the
|
||||
// Teleport Verify mobile app via a QR-code-encoded token.
|
||||
|
||||
@@ -79,6 +79,8 @@ import (
|
||||
_ "github.com/aws/smithy-go"
|
||||
_ "github.com/aws/smithy-go/transport/http"
|
||||
_ "github.com/beevik/etree"
|
||||
_ "github.com/boombuler/barcode"
|
||||
_ "github.com/boombuler/barcode/qr"
|
||||
_ "github.com/coreos/go-semver/semver"
|
||||
_ "github.com/crewjam/saml"
|
||||
_ "github.com/crewjam/saml/samlsp"
|
||||
|
||||
@@ -649,6 +649,14 @@
|
||||
"enroll"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"mobile_device"
|
||||
],
|
||||
"verbs": [
|
||||
"create_enroll_token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"db_service"
|
||||
|
||||
@@ -101,6 +101,7 @@ require (
|
||||
github.com/aws/smithy-go/tracing/smithyoteltracing v1.0.9
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.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/charlievieth/strcase v0.0.5
|
||||
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/v4 v4.0.0 // 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/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
|
||||
@@ -1329,6 +1329,7 @@ func unscopedDefinitionForBuiltinRole(clusterName string, recConfig readonly.Ses
|
||||
Rules: []types.Rule{
|
||||
types.NewRule(types.Wildcard, services.RW()),
|
||||
types.NewRule(types.KindDevice, append(services.RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
|
||||
types.NewRule(types.KindMobileDevice, []string{types.VerbCreateEnrollToken}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
+23
-13
@@ -180,6 +180,7 @@ func NewPresetEditorRole() types.Role {
|
||||
types.NewRule(types.KindDatabaseCertificate, RW()),
|
||||
types.NewRule(types.KindInstaller, RW()),
|
||||
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.KindInstance, RO()),
|
||||
types.NewRule(types.KindLoginRule, RW()),
|
||||
@@ -492,6 +493,7 @@ func NewPresetDeviceAdminRole(buildType string) types.Role {
|
||||
Allow: types.RoleConditions{
|
||||
Rules: []types.Rule{
|
||||
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
|
||||
// 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
|
||||
// on auth server restart. Rules defined in preset template should be
|
||||
// exactly the same rule when added here.
|
||||
func defaultAllowRules() map[string][]types.Rule {
|
||||
return defaultAllowRulesMap
|
||||
func defaultAllowRules(buildType string) map[string][]types.Rule {
|
||||
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.
|
||||
@@ -1299,7 +1309,7 @@ func AddRoleDefaults(ctx context.Context, buildType string, role types.Role) (ty
|
||||
}
|
||||
|
||||
// Resource Rules
|
||||
defaultRules, ok := defaultAllowRules()[role.GetName()]
|
||||
defaultRules, ok := defaultAllowRules(buildType)[role.GetName()]
|
||||
if ok {
|
||||
existingRules := append(role.GetRules(types.Allow), role.GetRules(types.Deny)...)
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestAddRoleDefaults(t *testing.T) {
|
||||
},
|
||||
Spec: types.RoleSpecV6{
|
||||
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{
|
||||
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{
|
||||
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{
|
||||
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
|
||||
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
|
||||
Rules: defaultAllowRules()[teleport.PresetAccessRoleName],
|
||||
Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
|
||||
GitHubPermissions: []types.GitHubPermission{{
|
||||
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
|
||||
}},
|
||||
@@ -199,7 +199,7 @@ func TestAddRoleDefaults(t *testing.T) {
|
||||
Allow: types.RoleConditions{
|
||||
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
|
||||
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
|
||||
Rules: defaultAllowRules()[teleport.PresetAccessRoleName],
|
||||
Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
|
||||
GitHubPermissions: []types.GitHubPermission{{
|
||||
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
|
||||
}},
|
||||
@@ -221,7 +221,7 @@ func TestAddRoleDefaults(t *testing.T) {
|
||||
Allow: types.RoleConditions{
|
||||
DatabaseServiceLabels: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseServiceLabels,
|
||||
DatabaseRoles: defaultAllowLabels(false)[teleport.PresetAccessRoleName].DatabaseRoles,
|
||||
Rules: defaultAllowRules()[teleport.PresetAccessRoleName],
|
||||
Rules: defaultAllowRules(modules.BuildOSS)[teleport.PresetAccessRoleName],
|
||||
GitHubPermissions: []types.GitHubPermission{{
|
||||
Organizations: defaultGitHubOrgs()[teleport.PresetAccessRoleName],
|
||||
}},
|
||||
@@ -288,7 +288,7 @@ func TestAddRoleDefaults(t *testing.T) {
|
||||
},
|
||||
},
|
||||
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{
|
||||
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 {
|
||||
|
||||
@@ -35,6 +35,15 @@ type ResourceAccess struct {
|
||||
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
|
||||
// information as to what features the user is allowed to use.
|
||||
type UserACL struct {
|
||||
@@ -150,6 +159,8 @@ type UserACL struct {
|
||||
AutoUpdateAgentReport ResourceAccess `json:"autoUpdateAgentReport"`
|
||||
// Beam defines access to Beams
|
||||
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 {
|
||||
@@ -273,6 +284,10 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des
|
||||
beam = newAccess(userRoles, ctx, types.KindBeam)
|
||||
}
|
||||
|
||||
mobileDevice := MobileDeviceAccess{
|
||||
CreateEnrollToken: hasAccess(userRoles, ctx, types.KindMobileDevice, types.VerbCreateEnrollToken),
|
||||
}
|
||||
|
||||
return UserACL{
|
||||
AccessRequests: requestAccess,
|
||||
AppServers: appServerAccess,
|
||||
@@ -330,5 +345,6 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des
|
||||
AutoUpdateAgentRollout: autoUpdateAgentRollout,
|
||||
AutoUpdateAgentReport: autoUpdateAgentReport,
|
||||
Beam: beam,
|
||||
MobileDevice: mobileDevice,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ func TestNewUserACL(t *testing.T) {
|
||||
Resources: []string{types.KindBeam},
|
||||
Verbs: RW(),
|
||||
},
|
||||
{
|
||||
Resources: []string{types.KindMobileDevice},
|
||||
Verbs: []string{types.VerbCreateEnrollToken},
|
||||
},
|
||||
})
|
||||
|
||||
// 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
|
||||
require.Empty(t, cmp.Diff(userContext.Beam, denied))
|
||||
|
||||
require.True(t, userContext.MobileDevice.CreateEnrollToken)
|
||||
|
||||
// test enabling of the 'Use' verb
|
||||
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.Semaphore, ['readnosecrets']],
|
||||
[ResourceKind.Device, ['create_enroll_token', 'enroll']],
|
||||
[ResourceKind.MobileDevice, ['create_enroll_token']],
|
||||
[ResourceKind.AuditQuery, ['use']],
|
||||
[ResourceKind.SecurityReport, ['use']],
|
||||
[ResourceKind.Integration, ['use']],
|
||||
|
||||
@@ -91,6 +91,7 @@ export const allAccessAcl: Acl = {
|
||||
inferenceModel: fullAccess,
|
||||
inferenceSecret: fullAccess,
|
||||
beam: fullAccess,
|
||||
mobileDevice: { createEnrollToken: true },
|
||||
};
|
||||
|
||||
export function getAcl(cfg?: { noAccess: boolean }) {
|
||||
|
||||
@@ -313,6 +313,7 @@ export enum ResourceKind {
|
||||
Lock = 'lock',
|
||||
LoginRule = 'login_rule',
|
||||
MFADevice = 'mfa_device',
|
||||
MobileDevice = 'mobile_device',
|
||||
// Ignoring duplicate: KindNamespace = "namespace"
|
||||
NetworkRestrictions = 'network_restrictions',
|
||||
Node = 'node',
|
||||
|
||||
@@ -105,6 +105,8 @@ export function makeAcl(json): Acl {
|
||||
|
||||
const beam = json.beam || defaultAccess;
|
||||
|
||||
const mobileDevice = json.mobileDevice || defaultMobileDeviceAccess;
|
||||
|
||||
return {
|
||||
accessList,
|
||||
authConnectors,
|
||||
@@ -159,6 +161,7 @@ export function makeAcl(json): Acl {
|
||||
inferenceModel,
|
||||
inferenceSecret,
|
||||
beam,
|
||||
mobileDevice,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,3 +177,7 @@ export const defaultAccessWithUse = {
|
||||
...defaultAccess,
|
||||
use: false,
|
||||
};
|
||||
|
||||
export const defaultMobileDeviceAccess = {
|
||||
createEnrollToken: false,
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@ export interface AccessWithUse extends Access {
|
||||
use: boolean;
|
||||
}
|
||||
|
||||
export interface MobileDeviceAccess {
|
||||
createEnrollToken: boolean;
|
||||
}
|
||||
|
||||
export interface Acl {
|
||||
directorySharingEnabled: boolean;
|
||||
reviewRequests: boolean;
|
||||
@@ -130,6 +134,7 @@ export interface Acl {
|
||||
inferenceModel: Access;
|
||||
inferenceSecret: Access;
|
||||
beam: Access;
|
||||
mobileDevice: MobileDeviceAccess;
|
||||
}
|
||||
|
||||
// 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,
|
||||
remove: false,
|
||||
},
|
||||
mobileDevice: {
|
||||
createEnrollToken: false,
|
||||
},
|
||||
};
|
||||
|
||||
expect(response).toEqual({
|
||||
|
||||
@@ -322,4 +322,8 @@ export default class StoreUserContext extends Store<UserContext> {
|
||||
getBeamAccess() {
|
||||
return this.state.acl.beam;
|
||||
}
|
||||
|
||||
getMobileDeviceAccess() {
|
||||
return this.state.acl.mobileDevice;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user