diff --git a/api/types/system_role.go b/api/types/system_role.go index 38224b634d2de..2e8a311fa6b12 100644 --- a/api/types/system_role.go +++ b/api/types/system_role.go @@ -72,6 +72,11 @@ const ( RoleDiscovery SystemRole = "Discovery" // RoleOkta is a role for Okta nodes in the cluster RoleOkta SystemRole = "Okta" + // RoleMDM is the role for MDM services in the cluster. + // An MDM service, like Jamf Service, has the powers to manage the cluster's + // device inventory. + // Device Trust requires Teleport Enteprise. + RoleMDM SystemRole = "MDM" ) // roleMappings maps a set of allowed lowercase system role names @@ -97,6 +102,7 @@ var roleMappings = map[string]SystemRole{ "instance": RoleInstance, "discovery": RoleDiscovery, "okta": RoleOkta, + "mdm": RoleMDM, } // localServiceMappings is the subset of role mappings which happen to be true @@ -112,6 +118,7 @@ var localServiceMappings = map[SystemRole]struct{}{ RoleWindowsDesktop: {}, RoleDiscovery: {}, RoleOkta: {}, + RoleMDM: {}, } // LocalServiceMappings returns the subset of role mappings which happen diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index fd5b41dcca99d..b1b973d00252c 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -3827,7 +3827,9 @@ func TestLocalServiceRolesHavePermissionsForUploaderService(t *testing.T) { require.NoError(t, err) for _, role := range types.LocalServiceMappings() { - if role == types.RoleAuth { + // RoleMDM services don't create events by themselves, instead they rely on + // Auth to issue events. + if role == types.RoleAuth || role == types.RoleMDM { continue } t.Run(role.String(), func(t *testing.T) { diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index 8d98e8db759df..53a5256882ccc 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -890,6 +890,17 @@ func definitionForBuiltinRole(clusterName string, recConfig types.SessionRecordi }, }, }) + case types.RoleMDM: + return services.RoleFromSpec( + role.String(), + types.RoleSpecV6{ + Allow: types.RoleConditions{ + Namespaces: []string{types.Wildcard}, + Rules: []types.Rule{ + types.NewRule(types.KindDevice, services.RW()), + }, + }, + }) } return nil, trace.NotFound("builtin role %q is not recognized", role.String()) diff --git a/lib/authz/permissions_test.go b/lib/authz/permissions_test.go index 039dc076f69d6..cea1024f849a1 100644 --- a/lib/authz/permissions_test.go +++ b/lib/authz/permissions_test.go @@ -621,6 +621,36 @@ func TestAuthorizeWithVerbs(t *testing.T) { } } +func TestRoleSetForBuiltinRoles(t *testing.T) { + tests := []struct { + name string + clusterName string + recConfig types.SessionRecordingConfig + roles []types.SystemRole + assertRoleSet func(t *testing.T, rs services.RoleSet) + }{ + { + name: "RoleMDM is mapped", + clusterName: clusterName, + roles: []types.SystemRole{types.RoleMDM}, + assertRoleSet: func(t *testing.T, rs services.RoleSet) { + for i, r := range rs { + assert.NotEmpty(t, r.GetNamespaces(types.Allow), "RoleSetForBuiltinRoles: rs[%v]: role has no namespaces", i) + assert.NotEmpty(t, r.GetRules(types.Allow), "RoleSetForBuiltinRoles: rs[%v]: role has no rules", i) + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rs, err := RoleSetForBuiltinRoles(test.clusterName, test.recConfig, test.roles...) + require.NoError(t, err, "RoleSetForBuiltinRoles failed") + assert.NotEmpty(t, rs, "RoleSetForBuiltinRoles returned a nil RoleSet") + test.assertRoleSet(t, rs) + }) + } +} + // fakeCtxUser is used for auth.Context tests. type fakeCtxUser struct { types.User diff --git a/tool/tctl/common/token_command.go b/tool/tctl/common/token_command.go index 52c8fc9fbbb1b..173a81fea2975 100644 --- a/tool/tctl/common/token_command.go +++ b/tool/tctl/common/token_command.go @@ -24,6 +24,7 @@ import ( "os" "sort" "strings" + "text/template" "time" "github.com/ghodss/yaml" @@ -42,6 +43,19 @@ import ( "github.com/gravitational/teleport/lib/utils" ) +var mdmTokenAddTemplate = template.Must( + template.New("mdmTokenAdd").Parse(`The invite token: {{.token}} +This token will expire in {{.minutes}} minutes. + +Use this token to add an MDM service to Teleport. + +> teleport start \ + --token={{.token}} \{{range .ca_pins}} + --ca-pin={{.}} \{{end}} + -c=/path/to/teleport.yaml + +`)) + // TokensCommand implements `tctl tokens` group of commands type TokensCommand struct { config *servicecfg.Config @@ -297,6 +311,12 @@ func (c *TokensCommand) Add(ctx context.Context, client auth.ClientI) error { "token": token, "minutes": c.ttl.Minutes(), }) + case roles.Include(types.RoleMDM): + return mdmTokenAddTemplate.Execute(c.stdout, map[string]interface{}{ + "token": token, + "minutes": c.ttl.Minutes(), + "ca_pins": caPins, + }) default: authServer := authServers[0].GetAddr()