[Search-based access requests] Include allowed resource IDs in user certs (#12494)

This commit is contained in:
Nic Klaassen 2022-05-27 09:23:18 -07:00 committed by GitHub
parent 2fbf94bf50
commit b55320f806
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 473 additions and 230 deletions

View file

@ -512,6 +512,8 @@ type NewWebSessionRequest struct {
LoginTime time.Time
// AccessRequests contains the UUIDs of the access requests currently in use.
AccessRequests []string
// RequestedResourceIDs optionally lists requested resources
RequestedResourceIDs []ResourceID
}
// Check validates the request.

View file

@ -443,6 +443,9 @@ const (
// CertExtensionGeneration counts the number of times a certificate has
// been renewed.
CertExtensionGeneration = "generation"
// CertExtensionAllowedResources lists the resources which this certificate
// should be allowed to access
CertExtensionAllowedResources = "allowed-resources"
)
// Note: when adding new providers to this list, consider updating the help message for --provider flag

View file

@ -0,0 +1,368 @@
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth
import (
"context"
"crypto/tls"
"strings"
"testing"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
func TestAccessRequest(t *testing.T) {
t.Parallel()
ctx := context.Background()
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
server, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
roleSpecs := map[string]types.RoleSpecV5{
// admins is the role to be requested
"admins": types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{"root"},
NodeLabels: types.Labels{
"owner": []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
// operators can request the admins role
"operators": types.RoleSpecV5{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
},
},
// responders can request the admins role but only with a
// search-based request limited to specific resources
"responders": types.RoleSpecV5{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: []string{"admins"},
},
},
},
"empty": types.RoleSpecV5{},
}
for roleName, roleSpec := range roleSpecs {
role, err := types.NewRole(roleName, roleSpec)
require.NoError(t, err)
err = server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
}
userDesc := map[string]struct {
roles []string
}{
"admin": {
roles: []string{"admins"},
},
"responder": {
roles: []string{"responders"},
},
"operator": {
roles: []string{"operators"},
},
"nobody": {
roles: []string{"empty"},
},
}
for name, desc := range userDesc {
user, err := types.NewUser(name)
require.NoError(t, err)
user.SetRoles(desc.roles)
err = server.Auth().UpsertUser(user)
require.NoError(t, err)
}
resourceIDs := []types.ResourceID{{
ClusterName: "cluster-one",
Kind: "node",
Name: "some-uuid",
}}
privKey, pubKey, err := native.GenerateKeyPair()
require.NoError(t, err)
testCases := []struct {
desc string
requester string
reviewer string
expectRequestableRoles []string
requestRoles []string
expectRoles []string
requestResources []types.ResourceID
expectRequestError error
expectReviewError error
}{
{
desc: "role request",
requester: "operator",
reviewer: "admin",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"admins"},
expectRoles: []string{"operators", "admins"},
},
{
desc: "no requestable roles",
requester: "nobody",
requestRoles: []string{"admins"},
expectRequestError: trace.BadParameter(`user "nobody" can not request role "admins"`),
},
{
desc: "role not allowed",
requester: "operator",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"super-admins"},
expectRequestError: trace.BadParameter(`user "operator" can not request role "super-admins"`),
},
{
desc: "review own request",
requester: "operator",
reviewer: "operator",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"admins"},
expectReviewError: trace.AccessDenied(`user "operator" cannot submit reviews`),
},
{
desc: "search-based request",
requester: "responder",
reviewer: "admin",
requestResources: resourceIDs,
expectRoles: []string{"responders", "admins"},
},
{
desc: "no search_as_roles",
requester: "nobody",
requestResources: resourceIDs,
expectRequestError: trace.AccessDenied(`user does not have any "search_as_roles" which are valid for this request`),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
requester := TestUser(tc.requester)
requesterClient, err := server.NewClient(requester)
require.NoError(t, err)
// generateCerts executes a GenerateUserCerts request, optionally applying
// one or more access-requests to the certificate.
generateCerts := func(reqIDs ...string) (*proto.Certs, error) {
return requesterClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
Format: constants.CertificateFormatStandard,
AccessRequests: reqIDs,
})
}
// sanity check we can get certs with no access request
certs, err := generateCerts()
require.NoError(t, err)
// should have no logins, requests, or resources in ssh cert
checkCerts(t, certs, userDesc[tc.requester].roles, nil, nil, nil)
// requestable roles should be correct
caps, err := requesterClient.GetAccessCapabilities(ctx, types.AccessCapabilitiesRequest{
RequestableRoles: true,
})
require.NoError(t, err)
require.Equal(t, tc.expectRequestableRoles, caps.RequestableRoles)
// create the access request object
req, err := services.NewAccessRequestWithResources(tc.requester, tc.requestRoles, tc.requestResources)
require.NoError(t, err)
// send the request to the auth server
err = requesterClient.CreateAccessRequest(ctx, req)
require.ErrorIs(t, err, tc.expectRequestError)
if tc.expectRequestError != nil {
return
}
// try logging in with request in PENDING state (should fail)
_, err = generateCerts(req.GetName())
require.ErrorIs(t, err, trace.AccessDenied("access request %q is awaiting approval", req.GetName()))
reviewer := TestUser(tc.reviewer)
reviewerClient, err := server.NewClient(reviewer)
require.NoError(t, err)
// approve the request
req, err = reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: req.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_APPROVED,
},
})
require.ErrorIs(t, err, tc.expectReviewError)
if tc.expectReviewError != nil {
return
}
require.Equal(t, types.RequestState_APPROVED, req.GetState())
// log in now that request has been approved
certs, err = generateCerts(req.GetName())
require.NoError(t, err)
// cert should have login from requested role, the access request
// should be in the cert, and any requested resources should be
// encoded in the cert
checkCerts(t,
certs,
tc.expectRoles,
[]string{"root"},
[]string{req.GetName()},
tc.requestResources)
elevatedCert, err := tls.X509KeyPair(certs.TLS, privKey)
require.NoError(t, err)
elevatedClient := server.NewClientWithCert(elevatedCert)
// renew elevated certs
newCerts, err := elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
// no new access requests
AccessRequests: nil,
})
require.NoError(t, err)
// inspite of providing no access requests, we still have elevated
// roles and the certicate shows the original access request
checkCerts(t,
newCerts,
tc.expectRoles,
[]string{"root"},
[]string{req.GetName()},
tc.requestResources)
// attempt to apply request in DENIED state (should fail)
require.NoError(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_DENIED,
}))
_, err = generateCerts(req.GetName())
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
require.Error(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_PENDING,
}))
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
require.Error(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_APPROVED,
}))
// ensure that identities with requests in the DENIED state can't reissue new certs.
_, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
// no new access requests
AccessRequests: nil,
})
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
})
}
}
// checkCerts checks that the ssh and tls certs include the given roles, logins,
// accessRequests, and resourceIDs
func checkCerts(t *testing.T,
certs *proto.Certs,
roles []string,
logins []string,
accessRequests []string,
resourceIDs []types.ResourceID,
) {
t.Helper()
// Parse SSH cert.
sshCert, err := sshutils.ParseCertificate(certs.SSH)
require.NoError(t, err)
// Parse TLS cert.
tlsCert, err := tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
tlsIdentity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
require.NoError(t, err)
// Make sure both certs have the expected roles.
rawSSHCertRoles := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]
sshCertRoles, err := services.UnmarshalCertRoles(rawSSHCertRoles)
require.NoError(t, err)
require.ElementsMatch(t, roles, sshCertRoles)
require.ElementsMatch(t, roles, tlsIdentity.Groups)
// Make sure both certs have the expected logins/principals.
for _, certLogins := range [][]string{sshCert.ValidPrincipals, tlsIdentity.Principals} {
// filter out invalid logins placed in the cert
validCertLogins := []string{}
for _, certLogin := range certLogins {
if !strings.HasPrefix(certLogin, "-teleport") {
validCertLogins = append(validCertLogins, certLogin)
}
}
require.ElementsMatch(t, logins, validCertLogins)
}
// Make sure both certs have the expected access requests, if any.
rawSSHCertAccessRequests := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportActiveRequests]
sshCertAccessRequests := services.RequestIDs{}
if len(rawSSHCertAccessRequests) > 0 {
require.NoError(t, sshCertAccessRequests.Unmarshal([]byte(rawSSHCertAccessRequests)))
}
require.ElementsMatch(t, accessRequests, sshCertAccessRequests.AccessRequests)
require.ElementsMatch(t, accessRequests, tlsIdentity.ActiveRequests)
// Make sure both certs have the expected allowed resources, if any.
for _, certResourcesStr := range []string{
sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources],
tlsIdentity.AllowedResourceIDs,
} {
certResources, err := services.ResourceIDsFromString(certResourcesStr)
require.NoError(t, err)
require.ElementsMatch(t, resourceIDs, certResources)
}
}

View file

@ -745,6 +745,9 @@ type certRequest struct {
// activeRequests tracks privilege escalation requests applied
// during the construction of the certificate.
activeRequests services.RequestIDs
// requestedResourceIDs lists the resources to which access has been
// requested.
requestedResourceIDs []types.ResourceID
// appSessionID is the session ID of the application session.
appSessionID string
// appPublicAddr is the public address of the application.
@ -1053,6 +1056,11 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
// All users have access to this and join RBAC rules are checked after the connection is established.
allowedLogins = append(allowedLogins, "-teleport-internal-join")
requestedResourcesStr, err := services.ResourceIDsToString(req.requestedResourceIDs)
if err != nil {
return nil, trace.Wrap(err)
}
params := services.UserCertParams{
CASigner: caSigner,
CASigningAlg: sshutils.GetSigningAlgName(userCA),
@ -1075,6 +1083,7 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
Renewable: req.renewable,
Generation: req.generation,
CertificateExtensions: req.checker.CertificateExtensions(),
AllowedResourceIDs: requestedResourcesStr,
}
sshCert, err := a.Authority.GenerateUserCert(params)
if err != nil {
@ -1146,15 +1155,16 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
Username: req.dbUser,
Database: req.dbName,
},
DatabaseNames: dbNames,
DatabaseUsers: dbUsers,
MFAVerified: req.mfaVerified,
ClientIP: req.clientIP,
AWSRoleARNs: roleARNs,
ActiveRequests: req.activeRequests.AccessRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
DatabaseNames: dbNames,
DatabaseUsers: dbUsers,
MFAVerified: req.mfaVerified,
ClientIP: req.clientIP,
AWSRoleARNs: roleARNs,
ActiveRequests: req.activeRequests.AccessRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
AllowedResourceIDs: requestedResourcesStr,
}
subject, err := identity.Subject()
if err != nil {
@ -1819,19 +1829,21 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
}
accessRequests := identity.ActiveRequests
requestedResourceIDs := []types.ResourceID{}
if req.AccessRequestID != "" {
newRoles, requestExpiry, err := a.getRolesAndExpiryFromAccessRequest(ctx, req.User, req.AccessRequestID)
accessRequest, err := a.getValidatedAccessRequest(ctx, req.User, req.AccessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}
roles = append(roles, newRoles...)
roles = append(roles, accessRequest.GetRoles()...)
roles = apiutils.Deduplicate(roles)
accessRequests = apiutils.Deduplicate(append(accessRequests, req.AccessRequestID))
requestedResourceIDs = accessRequest.GetRequestedResourceIDs()
// Let session expire with the shortest expiry time.
if expiresAt.After(requestExpiry) {
expiresAt = requestExpiry
if expiresAt.After(accessRequest.GetAccessExpiry()) {
expiresAt = accessRequest.GetAccessExpiry()
}
}
@ -1862,11 +1874,12 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
sessionTTL := utils.ToTTL(a.clock, expiresAt)
sess, err := a.NewWebSession(types.NewWebSessionRequest{
User: req.User,
Roles: roles,
Traits: traits,
SessionTTL: sessionTTL,
AccessRequests: accessRequests,
User: req.User,
Roles: roles,
Traits: traits,
SessionTTL: sessionTTL,
AccessRequests: accessRequests,
RequestedResourceIDs: requestedResourceIDs,
})
if err != nil {
return nil, trace.Wrap(err)
@ -1882,7 +1895,7 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
return sess, nil
}
func (a *Server) getRolesAndExpiryFromAccessRequest(ctx context.Context, user, accessRequestID string) ([]string, time.Time, error) {
func (a *Server) getValidatedAccessRequest(ctx context.Context, user, accessRequestID string) (types.AccessRequest, error) {
reqFilter := types.AccessRequestFilter{
User: user,
ID: accessRequestID,
@ -1890,37 +1903,32 @@ func (a *Server) getRolesAndExpiryFromAccessRequest(ctx context.Context, user, a
reqs, err := a.GetAccessRequests(ctx, reqFilter)
if err != nil {
return nil, time.Time{}, trace.Wrap(err)
return nil, trace.Wrap(err)
}
if len(reqs) < 1 {
return nil, time.Time{}, trace.NotFound("access request %q not found", accessRequestID)
return nil, trace.NotFound("access request %q not found", accessRequestID)
}
req := reqs[0]
if len(req.GetRequestedResourceIDs()) > 0 {
// TODO(nic): handle search-based access requests #10887
return nil, time.Time{}, trace.BadParameter("search-based access requests are not yet supported")
}
if !req.GetState().IsApproved() {
if req.GetState().IsDenied() {
return nil, time.Time{}, trace.AccessDenied("access request %q has been denied", accessRequestID)
return nil, trace.AccessDenied("access request %q has been denied", accessRequestID)
}
return nil, time.Time{}, trace.BadParameter("access request %q is awaiting approval", accessRequestID)
return nil, trace.AccessDenied("access request %q is awaiting approval", accessRequestID)
}
if err := services.ValidateAccessRequestForUser(a, req); err != nil {
return nil, time.Time{}, trace.Wrap(err)
return nil, trace.Wrap(err)
}
accessExpiry := req.GetAccessExpiry()
if accessExpiry.Before(a.GetClock().Now()) {
return nil, time.Time{}, trace.BadParameter("access request %q has expired", accessRequestID)
return nil, trace.BadParameter("access request %q has expired", accessRequestID)
}
return req.GetRoles(), accessExpiry, nil
return req, nil
}
// CreateWebSession creates a new web session for user without any
@ -2329,12 +2337,13 @@ func (a *Server) NewWebSession(req types.NewWebSessionRequest) (types.WebSession
sessionTTL = checker.AdjustSessionTTL(apidefaults.CertDuration)
}
certs, err := a.generateUserCert(certRequest{
user: user,
ttl: sessionTTL,
publicKey: pub,
checker: checker,
traits: req.Traits,
activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests},
user: user,
ttl: sessionTTL,
publicKey: pub,
checker: checker,
traits: req.Traits,
activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests},
requestedResourceIDs: req.RequestedResourceIDs,
})
if err != nil {
return nil, trace.Wrap(err)

View file

@ -2143,19 +2143,25 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
return nil, trace.Wrap(err)
}
var requestedResourceIDs []types.ResourceID
if len(req.AccessRequests) > 0 {
// add any applicable access request values.
req.AccessRequests = apiutils.Deduplicate(req.AccessRequests)
for _, reqID := range req.AccessRequests {
newRoles, accessRequestExpiry, err := a.authServer.getRolesAndExpiryFromAccessRequest(ctx, req.Username, reqID)
accessRequest, err := a.authServer.getValidatedAccessRequest(ctx, req.Username, reqID)
if err != nil {
return nil, trace.Wrap(err)
}
if accessRequestExpiry.Before(req.Expires) {
if accessRequest.GetAccessExpiry().Before(req.Expires) {
// cannot generate a cert that would outlive the access request
req.Expires = accessRequestExpiry
req.Expires = accessRequest.GetAccessExpiry()
}
roles = append(roles, newRoles...)
roles = append(roles, accessRequest.GetRoles()...)
if len(accessRequest.GetRequestedResourceIDs()) > 0 && len(requestedResourceIDs) > 0 {
return nil, trace.BadParameter("cannot generate certificate with multiple search-based access requests")
}
requestedResourceIDs = accessRequest.GetRequestedResourceIDs()
}
// nothing prevents an access-request from including roles already possessed by the
// user, so we must make sure to trim duplicate roles.
@ -2257,6 +2263,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
activeRequests: services.RequestIDs{
AccessRequests: req.AccessRequests,
},
requestedResourceIDs: requestedResourceIDs,
}
if user.GetName() != a.context.User.GetName() {
certReq.impersonator = a.context.User.GetName()

View file

@ -274,6 +274,9 @@ func (k *Keygen) GenerateUserCertWithoutValidation(c services.UserCertParams) ([
if c.Generation > 0 {
cert.Permissions.Extensions[teleport.CertExtensionGeneration] = fmt.Sprint(c.Generation)
}
if c.AllowedResourceIDs != "" {
cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = c.AllowedResourceIDs
}
for _, extension := range c.CertificateExtensions {
// TODO(lxea): update behavior when non ssh, non extensions are supported.

View file

@ -1350,195 +1350,6 @@ func (s *TLSSuite) TestGetCertAuthority(c *check.C) {
fixtures.ExpectAccessDenied(c, err)
}
func (s *TLSSuite) TestAccessRequest(c *check.C) {
priv, pub, err := native.GenerateKeyPair()
c.Assert(err, check.IsNil)
// make sure we can parse the private and public key
privateKey, err := ssh.ParseRawPrivateKey(priv)
c.Assert(err, check.IsNil)
_, err = tlsca.MarshalPublicKeyFromPrivateKeyPEM(privateKey)
c.Assert(err, check.IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
c.Assert(err, check.IsNil)
// create a user with one requestable role
user := "user1"
role := "some-role"
_, err = CreateUserRoleAndRequestable(s.server.Auth(), user, role)
c.Assert(err, check.IsNil)
testUser := TestUser(user)
testUser.TTL = time.Hour
userClient, err := s.server.NewClient(testUser)
c.Assert(err, check.IsNil)
// Verify that user has correct requestable roles
caps, err := userClient.GetAccessCapabilities(context.TODO(), types.AccessCapabilitiesRequest{
RequestableRoles: true,
})
c.Assert(err, check.IsNil)
c.Assert(caps.RequestableRoles, check.DeepEquals, []string{role})
// create a user with no requestable roles
user2, _, err := CreateUserAndRole(s.server.Auth(), "user2", []string{"user2"})
c.Assert(err, check.IsNil)
testUser2 := TestUser(user2.GetName())
testUser2.TTL = time.Hour
userClient2, err := s.server.NewClient(testUser2)
c.Assert(err, check.IsNil)
// verify that no requestable roles are shown for user2
caps2, err := userClient2.GetAccessCapabilities(context.TODO(), types.AccessCapabilitiesRequest{
RequestableRoles: true,
})
c.Assert(err, check.IsNil)
c.Assert(caps2.RequestableRoles, check.HasLen, 0)
// create an allowable access request for user1
req, err := services.NewAccessRequest(user, role)
c.Assert(err, check.IsNil)
c.Assert(userClient.CreateAccessRequest(context.TODO(), req), check.IsNil)
// sanity check; ensure that roles for which no `allow` directive
// exists cannot be requested.
badReq, err := services.NewAccessRequest(user, "some-fake-role")
c.Assert(err, check.IsNil)
c.Assert(userClient.CreateAccessRequest(context.TODO(), badReq), check.NotNil)
// generateCerts executes a GenerateUserCerts request, optionally applying
// one or more access-requests to the certificate.
generateCerts := func(reqIDs ...string) (*proto.Certs, error) {
return userClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{
PublicKey: pub,
Username: user,
Expires: time.Now().Add(time.Hour).UTC(),
Format: constants.CertificateFormatStandard,
AccessRequests: reqIDs,
})
}
// certContainsRole checks if a PEM encoded TLS cert contains the
// specified role.
certContainsRole := func(tlsCert []byte, role string) bool {
cert, err := tlsca.ParseCertificatePEM(tlsCert)
c.Assert(err, check.IsNil)
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
c.Assert(err, check.IsNil)
return apiutils.SliceContainsStr(identity.Groups, role)
}
// certRequests extracts the active requests from a PEM encoded TLS cert.
certRequests := func(tlsCert []byte) []string {
cert, err := tlsca.ParseCertificatePEM(tlsCert)
c.Assert(err, check.IsNil)
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
c.Assert(err, check.IsNil)
return identity.ActiveRequests
}
// certLogins extracts the logins from an ssh certificate
certLogins := func(sshCert []byte) []string {
cert, err := sshutils.ParseCertificate(sshCert)
c.Assert(err, check.IsNil)
return cert.ValidPrincipals
}
// sanity check; ensure that role is not held if no request is applied.
userCerts, err := generateCerts()
c.Assert(err, check.IsNil)
if certContainsRole(userCerts.TLS, role) {
c.Errorf("unexpected role %s", role)
}
// ensure that the default identity doesn't have any active requests
c.Assert(certRequests(userCerts.TLS), check.HasLen, 0)
// verify that cert for user with no static logins is generated with
// exactly two logins and that the one that isn't a join principal is an invalid unix login (indicated
// by preceding dash (-).
logins := certLogins(userCerts.SSH)
c.Assert(len(logins), check.Equals, 2)
c.Assert(rune(logins[0][0]), check.Equals, '-')
// attempt to apply request in PENDING state (should fail)
_, err = generateCerts(req.GetName())
c.Assert(err, check.NotNil)
ctx := context.Background()
// verify that user does not have the ability to approve their own request (not a special case, this
// user just wasn't created with the necessary roles for request management).
c.Assert(userClient.SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.NotNil)
// attempt to apply request in APPROVED state (should succeed)
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.IsNil)
userCerts, err = generateCerts(req.GetName())
c.Assert(err, check.IsNil)
// ensure that the requested role was actually applied to the cert
if !certContainsRole(userCerts.TLS, role) {
c.Errorf("missing requested role %s", role)
}
// ensure that the request is stored in the certs
c.Assert(certRequests(userCerts.TLS), check.DeepEquals, []string{req.GetName()})
// verify that dynamically applied role granted a login,
// which is is valid and has replaced the dummy login.
logins = certLogins(userCerts.SSH)
c.Assert(len(logins), check.Equals, 2)
c.Assert(rune(logins[0][0]), check.Not(check.Equals), '-')
elevatedCert, err := tls.X509KeyPair(userCerts.TLS, priv)
c.Assert(err, check.IsNil)
elevatedClient := s.server.NewClientWithCert(elevatedCert)
newCerts, err := elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pub,
Username: user,
Expires: time.Now().Add(time.Hour).UTC(),
Format: constants.CertificateFormatStandard,
// no new access requests
AccessRequests: nil,
})
c.Assert(err, check.IsNil)
// in spite of having no access requests, we still have elevated roles...
if !certContainsRole(newCerts.TLS, role) {
c.Errorf("missing requested role %s", role)
}
// ...and the certificate shows the access request
c.Assert(certRequests(newCerts.TLS), check.DeepEquals, []string{req.GetName()})
// attempt to apply request in DENIED state (should fail)
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_DENIED}), check.IsNil)
_, err = generateCerts(req.GetName())
c.Assert(err, check.NotNil)
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_PENDING}), check.NotNil)
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.NotNil)
// ensure that identities with requests in the DENIED state can't reissue new certs.
_, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pub,
Username: user,
Expires: time.Now().Add(time.Hour).UTC(),
Format: constants.CertificateFormatStandard,
// no new access requests
AccessRequests: nil,
})
c.Assert(err, check.NotNil)
}
func (s *TLSSuite) TestPluginData(c *check.C) {
priv, pub, err := native.GenerateKeyPair()
c.Assert(err, check.IsNil)

View file

@ -1285,6 +1285,9 @@ func MarshalAccessRequest(accessRequest types.AccessRequest, opts ...MarshalOpti
}
func ResourceIDsToString(ids []types.ResourceID) (string, error) {
if len(ids) == 0 {
return "", nil
}
bytes, err := utils.FastMarshal(ids)
if err != nil {
return "", trace.BadParameter("failed to marshal resource IDs to JSON: %v", err)
@ -1293,6 +1296,9 @@ func ResourceIDsToString(ids []types.ResourceID) (string, error) {
}
func ResourceIDsFromString(raw string) ([]types.ResourceID, error) {
if raw == "" {
return nil, nil
}
resourceIDs := []types.ResourceID{}
if err := utils.FastUnmarshal([]byte(raw), &resourceIDs); err != nil {
return nil, trace.BadParameter("failed to parse resource IDs from JSON: %v", err)

View file

@ -298,6 +298,8 @@ type UserCertParams struct {
Renewable bool
// Generation counts the number of times a certificate has been renewed.
Generation uint64
// AllowedResourceIDs lists the resources the user should be able to access.
AllowedResourceIDs string
}
// CheckAndSetDefaults checks the user certificate parameters

View file

@ -160,6 +160,13 @@ func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityCon
}
identity.Generation = generation
}
if allowedResourcesStr, ok := certificate.Extensions[teleport.CertExtensionAllowedResources]; ok {
allowedResourceIDs, err := services.ResourceIDsFromString(allowedResourcesStr)
if err != nil {
return IdentityContext{}, trace.Wrap(err)
}
identity.AllowedResourceIDs = allowedResourceIDs
}
return identity, nil
}

View file

@ -214,6 +214,10 @@ type IdentityContext struct {
// Generation counts the number of times this identity's certificate has
// been renewed.
Generation uint64
// AllowedResourceIDs lists the resources this identity should be allowed to
// access
AllowedResourceIDs []types.ResourceID
}
// ServerContext holds session specific context, such as SSH auth agents, PTYs,

View file

@ -141,6 +141,9 @@ type Identity struct {
Renewable bool
// Generation counts the number of times this certificate has been renewed.
Generation uint64
// AllowedResourceIDs lists the resources the identity should be allowed to
// access.
AllowedResourceIDs string
}
// RouteToApp holds routing information for applications.
@ -361,6 +364,10 @@ var (
// requests to generate new certificates using this certificate should be
// denied.
DisallowReissueASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 9}
// AllowedResourcesASN1ExtensionOID is an extension OID used to list the
// resources which the certificate should be able to grant access to
AllowedResourcesASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 10}
)
// Subject converts identity to X.509 subject name
@ -565,6 +572,15 @@ func (id *Identity) Subject() (pkix.Name, error) {
)
}
if id.AllowedResourceIDs != "" {
subject.ExtraNames = append(subject.ExtraNames,
pkix.AttributeTypeAndValue{
Type: AllowedResourcesASN1ExtensionOID,
Value: id.AllowedResourceIDs,
},
)
}
return subject, nil
}
@ -710,6 +726,11 @@ func FromSubject(subject pkix.Name, expires time.Time) (*Identity, error) {
}
id.Generation = generation
}
case attr.Type.Equal(AllowedResourcesASN1ExtensionOID):
val, ok := attr.Value.(string)
if ok {
id.AllowedResourceIDs = val
}
}
}