feat: restrict login to specific org by id (scope) (#4294)

* feat: add new org scope

* change default of UserLoginMustBeDomain to false

* return resource owner claims

* fix: use email style for first user

* fix: ensure email style for default users (backwards compatibility)

* change to external domain (as it was before UserLoginMustBeDomain change)

* update e2e tests to use email style usernames

* document new scope

* lint e2e

Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com>
This commit is contained in:
Livio Spring
2022-09-23 14:08:10 +02:00
committed by GitHub
parent c98170c19b
commit 7dfa1925cc
17 changed files with 114 additions and 19 deletions

View File

@@ -41,7 +41,7 @@ func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequ
}
func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) {
id, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.DefaultInstance))
id, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
if err != nil {
return nil, err
}

View File

@@ -1,17 +1,20 @@
package system
import (
"strings"
"golang.org/x/text/language"
instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance"
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
)
func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInstance command.InstanceSetup) *command.InstanceSetup {
func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInstance command.InstanceSetup, externalDomain string) *command.InstanceSetup {
if req.InstanceName != "" {
defaultInstance.InstanceName = req.InstanceName
defaultInstance.Org.Name = req.InstanceName
@@ -40,6 +43,11 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst
}
}
}
// check if default username is email style or else append @<orgname>.<custom-domain>
// this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !defaultInstance.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(defaultInstance.Org.Human.Username, "@") {
defaultInstance.Org.Human.Username = defaultInstance.Org.Human.Username + "@" + domain.NewIAMDomainName(defaultInstance.Org.Name, externalDomain)
}
if req.OwnerUserName != "" {
defaultInstance.Org.Human.Username = req.OwnerUserName
}

View File

@@ -25,25 +25,29 @@ type Server struct {
command *command.Commands
query *query.Queries
administrator repository.AdministratorRepository
DefaultInstance command.InstanceSetup
defaultInstance command.InstanceSetup
externalDomain string
}
type Config struct {
Repository eventsourcing.Config
}
func CreateServer(command *command.Commands,
func CreateServer(
command *command.Commands,
query *query.Queries,
repo repository.Repository,
database string,
defaultInstance command.InstanceSetup,
externalDomain string,
) *Server {
return &Server{
command: command,
query: query,
administrator: repo,
database: database,
DefaultInstance: defaultInstance,
defaultInstance: defaultInstance,
externalDomain: externalDomain,
}
}

View File

@@ -95,6 +95,13 @@ func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string
scopes = scopes[:len(scopes)-1]
}
}
if strings.HasPrefix(scope, domain.OrgIDScope) {
if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner {
scopes[i] = scopes[len(scopes)-1]
scopes[len(scopes)-1] = ""
scopes = scopes[:len(scopes)-1]
}
}
}
return scopes, nil
}
@@ -251,6 +258,16 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
userInfo.AppendClaims(domain.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope))
}
if strings.HasPrefix(scope, domain.OrgIDScope) {
userInfo.AppendClaims(domain.OrgIDClaim, strings.TrimPrefix(scope, domain.OrgIDScope))
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return err
}
for claim, value := range resourceOwnerClaims {
userInfo.AppendClaims(claim, value)
}
}
}
}
if len(roles) == 0 || applicationID == "" {
@@ -289,9 +306,20 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie
}
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
} else if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
}
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
claims = appendClaim(claims, domain.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope))
}
if strings.HasPrefix(scope, domain.OrgIDScope) {
claims = appendClaim(claims, domain.OrgIDClaim, strings.TrimPrefix(scope, domain.OrgIDScope))
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return nil, err
}
for claim, value := range resourceOwnerClaims {
claims = appendClaim(claims, claim, value)
}
}
}
if len(roles) == 0 || clientID == "" {
return claims, nil

View File

@@ -103,6 +103,9 @@ func (c *Client) IsScopeAllowed(scope string) bool {
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
return true
}
if strings.HasPrefix(scope, domain.OrgIDScope) {
return true
}
if strings.HasPrefix(scope, domain.ProjectIDScope) {
return true
}

View File

@@ -507,7 +507,7 @@ func (l *Login) isDisplayLoginNameSuffix(authReq *domain.AuthRequest) bool {
if authReq == nil {
return false
}
if authReq.RequestedOrgID == "" {
if authReq.RequestedOrgID == "" || !authReq.RequestedOrgDomain {
return false
}
return authReq.LabelPolicy != nil && !authReq.LabelPolicy.HideLoginNameSuffix

View File

@@ -632,8 +632,14 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
loginName = strings.TrimSpace(loginName)
preferredLoginName := loginName
if request.RequestedOrgID != "" {
if request.RequestedOrgID != "" {
preferredLoginName += "@" + request.RequestedPrimaryDomain
if request.RequestedOrgDomain {
domainPolicy, err := repo.getDomainPolicy(ctx, request.RequestedOrgID)
if err != nil {
return err
}
if domainPolicy.UserLoginMustBeDomain {
preferredLoginName += "@" + request.RequestedPrimaryDomain
}
}
user, err = repo.View.UserByLoginNameAndResourceOwner(preferredLoginName, request.RequestedOrgID, request.InstanceID)
} else {
@@ -1058,7 +1064,23 @@ func (repo *AuthRequestRepo) hasSucceededPage(ctx context.Context, request *doma
return app.OIDCConfig.AppType == domain.OIDCApplicationTypeNative, nil
}
func (repo *AuthRequestRepo) getDomainPolicy(ctx context.Context, orgID string) (*query.DomainPolicy, error) {
return repo.Query.DomainPolicyByOrg(ctx, false, orgID)
}
func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *domain.AuthRequest) error {
orgID := request.GetScopeOrgID()
if orgID != "" {
org, err := orgViewProvider.OrgByID(ctx, false, orgID)
if err != nil {
return err
}
request.RequestedOrgID = org.ID
request.RequestedOrgName = org.Name
request.RequestedPrimaryDomain = org.Domain
return nil
}
primaryDomain := request.GetScopeOrgPrimaryDomain()
if primaryDomain == "" {
return nil
@@ -1071,6 +1093,7 @@ func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *dom
request.RequestedOrgID = org.ID
request.RequestedOrgName = org.Name
request.RequestedPrimaryDomain = primaryDomain
request.RequestedOrgDomain = true
return nil
}

View File

@@ -37,6 +37,7 @@ type AuthRequest struct {
RequestedOrgID string
RequestedOrgName string
RequestedPrimaryDomain string
RequestedOrgDomain bool
ApplicationResourceOwner string
PrivateLabelingSetting PrivateLabelingSetting
SelectedIDPConfigID string
@@ -164,3 +165,15 @@ func (a *AuthRequest) GetScopeOrgPrimaryDomain() string {
}
return ""
}
func (a *AuthRequest) GetScopeOrgID() string {
switch request := a.Request.(type) {
case *AuthRequestOIDC:
for _, scope := range request.Scopes {
if strings.HasPrefix(scope, OrgIDScope) {
return strings.TrimPrefix(scope, OrgIDScope)
}
}
}
return ""
}

View File

@@ -32,7 +32,7 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator
}
func NewIAMDomainName(orgName, iamDomain string) string {
return strings.ToLower(strings.ReplaceAll(orgName, " ", "-") + "." + iamDomain)
return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(orgName), " ", "-") + "." + iamDomain)
}
type OrgDomainValidationType int32

View File

@@ -2,7 +2,9 @@ package domain
const (
OrgDomainPrimaryScope = "urn:zitadel:iam:org:domain:primary:"
OrgIDScope = "urn:zitadel:iam:org:id:"
OrgDomainPrimaryClaim = "urn:zitadel:iam:org:domain:primary"
OrgIDClaim = "urn:zitadel:iam:org:id"
ProjectIDScope = "urn:zitadel:iam:org:project:id:"
ProjectIDScopeZITADEL = "zitadel"
AudSuffix = ":aud"