package controller import ( "fmt" "github.com/go-openapi/runtime/middleware" "github.com/jmoiron/sqlx" "github.com/michaelquigley/df/dl" "github.com/openziti/edge-api/rest_model" "github.com/openziti/zrok/v2/controller/automation" "github.com/openziti/zrok/v2/controller/store" "github.com/openziti/zrok/v2/rest_model_zrok" "github.com/openziti/zrok/v2/rest_server_zrok/operations/share" "github.com/openziti/zrok/v2/sdk/golang/sdk" "github.com/openziti/zrok/v2/util" "github.com/pkg/errors" ) type shareHandler struct{} func newShareHandler() *shareHandler { return &shareHandler{} } func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zrok.Principal) middleware.Responder { trx, err := str.Begin() if err != nil { dl.Errorf("error starting transaction: %v", err) return share.NewShareInternalServerError() } defer func() { _ = trx.Rollback() }() // validate environment envZId := params.Body.EnvZID envId, err := h.validateEnvironment(envZId, principal, trx) if err != nil { dl.Errorf("environment validation failed: %v", err) return share.NewShareUnauthorized() } // check limits if err := h.checkLimits(envId, principal, params, trx); err != nil { dl.Errorf("limits error: %v", err) return share.NewShareUnauthorized() } // create share token var shrToken string if sdk.ShareMode(params.Body.ShareMode) == sdk.PrivateShareMode && params.Body.PrivateShareToken != "" { dl.Infof("private share requested share token '%v'", params.Body.PrivateShareToken) if util.IsValidShareToken(params.Body.PrivateShareToken, cfg.Names != nil && cfg.Names.DisableShareTokenProfanityCheck) { shrToken = params.Body.PrivateShareToken } else { dl.Errorf("requested private share token '%v' has invalid unique name", params.Body.PrivateShareToken) return share.NewShareConflict().WithPayload(rest_model_zrok.ErrorMessage(fmt.Sprintf("requested private share token '%v' has invalid unique name", params.Body.PrivateShareToken))) } } else { shrToken, err = createShareToken() if err != nil { dl.Error(err) return share.NewShareInternalServerError() } } // process namespace selections var frontendEndpoints []string var nameIds []int if sdk.ShareMode(params.Body.ShareMode) == sdk.PublicShareMode { frontendEndpoints, nameIds, err = h.processNameSelections(params.Body.NameSelections, shrToken, principal, trx) if err != nil { dl.Errorf("namespace selection processing failed: %v", err) return share.NewShareConflict().WithPayload(rest_model_zrok.ErrorMessage(err.Error())) } } // allocate resources based on share mode var shrZId string switch sdk.ShareMode(params.Body.ShareMode) { case sdk.PublicShareMode: interstitial, err := h.shouldUseInterstitial(params.Body.BackendMode, principal, trx) if err != nil { dl.Errorf("error determining interstitial setting for account '%v': %v", principal.Email, err) return share.NewShareInternalServerError() } shrZId, frontendEndpoints, err = h.allocatePublicResources(envZId, shrToken, frontendEndpoints, params, interstitial, trx) case sdk.PrivateShareMode: // check private share token availability if provided if params.Body.PrivateShareToken != "" { if err := h.checkPrivateShareTokenAvailability(shrToken); err != nil { dl.Errorf("private share token conflict: %v", err) return share.NewShareConflict().WithPayload(rest_model_zrok.ErrorMessage(err.Error())) } } shrZId, frontendEndpoints, err = h.allocatePrivateResources(envZId, shrToken, frontendEndpoints, params, trx) default: dl.Errorf("unknown share mode '%v'", params.Body.ShareMode) return share.NewShareInternalServerError() } if err != nil { dl.Errorf("error allocating share resources: %v", err) return share.NewShareInternalServerError() } // create share record shareId, err := h.createShareRecord(envId, shrZId, shrToken, params, frontendEndpoints, trx) if err != nil { dl.Errorf("error creating share record: %v", err) return share.NewShareInternalServerError() } if sdk.ShareMode(params.Body.ShareMode) == sdk.PublicShareMode { // create share name mappings for namespace selections for _, nameId := range nameIds { snm := &store.ShareNameMapping{ ShareId: shareId, NameId: nameId, } _, err := str.CreateShareNameMapping(snm, trx) if err != nil { dl.Errorf("error creating share name mapping for share '%v' and name '%v': %v", shareId, nameId, err) return share.NewShareInternalServerError() } } // send mapping updates to dynamic frontends after successful commit if err := h.processDynamicMappings(shrToken, nameIds, trx); err != nil { dl.Errorf("error sending mapping updates: %v", err) } } // handle access grants if closed permission mode if err := h.processAccessGrants(shareId, params.Body.AccessGrants, params.Body.PermissionMode, principal, trx); err != nil { dl.Errorf("error processing access grants: %v", err) return share.NewShareInternalServerError() } if err := trx.Commit(); err != nil { dl.Errorf("error committing share record: %v", err) return share.NewShareInternalServerError() } dl.Infof("recorded share '%v' with id '%v' for '%v'", shrToken, shareId, principal.Email) return share.NewShareCreated().WithPayload(&rest_model_zrok.ShareResponse{ FrontendProxyEndpoints: frontendEndpoints, ShareToken: shrToken, }) } func (h *shareHandler) validateEnvironment(envZId string, principal *rest_model_zrok.Principal, trx *sqlx.Tx) (int, error) { env, err := str.FindEnvironmentForAccount(envZId, int(principal.ID), trx) if err != nil { return 0, errors.Wrapf(err, "error finding environment '%v' for account '%v'", envZId, principal.Email) } return env.Id, nil } func (h *shareHandler) checkLimits(envId int, principal *rest_model_zrok.Principal, params share.ShareParams, trx *sqlx.Tx) error { if !principal.Limitless { if limitsAgent != nil { shareMode := sdk.ShareMode(params.Body.ShareMode) backendMode := sdk.BackendMode(params.Body.BackendMode) // we're going to skip reservation checking because we're moving name creation outside the scope of share // creation. the limits check for name creation will happen in the `/share/name` endpoint instead. ok, err := limitsAgent.CanCreateShare(int(principal.ID), envId, false, false, shareMode, backendMode, trx) if err != nil { return errors.Wrapf(err, "error checking share limits for '%v'", principal.Email) } if !ok { return errors.Errorf("share limit check failed for '%v'", principal.Email) } } } return nil } func (h *shareHandler) shouldUseInterstitial(backendMode string, principal *rest_model_zrok.Principal, trx *sqlx.Tx) (bool, error) { var skipInterstitial bool parsedBackendMode := sdk.BackendMode(backendMode) if parsedBackendMode != sdk.DriveBackendMode { var err error skipInterstitial, err = str.IsAccountGrantedSkipInterstitial(int(principal.ID), trx) if err != nil { return false, errors.Wrapf(err, "error checking skip interstitial for account '%v'", principal.Email) } } else { // always skip interstitial for drive backend mode skipInterstitial = true } return !skipInterstitial, nil } func (h *shareHandler) processNameSelections(selections []*rest_model_zrok.NameSelection, shrToken string, principal *rest_model_zrok.Principal, trx *sqlx.Tx) ([]string, []int, error) { var frontendEndpoints []string var nameIds []int for _, selection := range selections { // find namespace by token ns, err := str.FindNamespaceWithToken(selection.NamespaceToken, trx) if err != nil { return nil, nil, errors.Wrapf(err, "error finding namespace with token '%v'", selection.NamespaceToken) } var endpoint string var nameId int if selection.Name != "" { // user specified a name - validate ownership and availability name, err := str.FindNameByNamespaceAndName(ns.Id, selection.Name, trx) if err != nil { return nil, nil, errors.Wrapf(err, "error finding name '%v' in namespace '%v'", selection.Name, ns.Token) } // check if user owns this name if name.AccountId != int(principal.ID) { return nil, nil, errors.Errorf("user '%v' does not own name '%v' in namespace '%v'", principal.Email, selection.Name, ns.Token) } // check if there's already a share_name_mapping for this name existing, err := str.FindShareNameMappingsByNameId(name.Id, trx) if err != nil { return nil, nil, errors.Wrapf(err, "error checking existing share name mappings for name '%v'", selection.Name) } if len(existing) > 0 { return nil, nil, errors.Errorf("name '%v' in namespace '%v' is already in use by another share", selection.Name, ns.Token) } nameId = name.Id endpoint = util.NameInNamespace(name.Name, ns.Name) } else { // no name specified - generate one and create name record // check namespace permissions if !ns.Open { granted, err := str.CheckNamespaceGrant(ns.Id, int(principal.ID), trx) if err != nil { return nil, nil, errors.Wrapf(err, "error checking namespace grant for account '%v' and namespace '%v'", principal.Email, ns.Token) } if !granted { return nil, nil, errors.Errorf("account '%v' is not granted access to namespace '%v'", principal.Email, ns.Token) } } // create name record with reserved=false (dynamically allocated) name := &store.Name{ NamespaceId: ns.Id, Name: shrToken, AccountId: int(principal.ID), Reserved: false, } nameId, err = str.CreateName(name, trx) if err != nil { return nil, nil, errors.Wrapf(err, "error creating allocated name '%v' in namespace '%v' for account '%v'", shrToken, ns.Token, principal.Email) } endpoint = util.NameInNamespace(shrToken, ns.Name) } frontendEndpoints = append(frontendEndpoints, endpoint) nameIds = append(nameIds, nameId) } return frontendEndpoints, nameIds, nil } func (h *shareHandler) allocatePublicResources(envZId, shrToken string, frontendEndpoints []string, params share.ShareParams, interstitial bool, trx interface{}) (string, []string, error) { // get shared automation client ziti, err := automation.NewZitiAutomation(cfg.Ziti) if err != nil { return "", nil, errors.Wrap(err, "error getting ziti automation client") } // prepare auth users var authUsers []*sdk.AuthUserConfig for _, authUser := range params.Body.BasicAuthUsers { authUsers = append(authUsers, &sdk.AuthUserConfig{Username: authUser.Username, Password: authUser.Password}) } // parse auth scheme authScheme, err := sdk.ParseAuthScheme(params.Body.AuthScheme) if err != nil { return "", nil, errors.Wrap(err, "error parsing auth scheme") } // prepare oauth config var oauthCfg *sdk.OauthConfig if authScheme == sdk.Oauth { oauthCfg = &sdk.OauthConfig{ Provider: params.Body.OauthProvider, EmailDomains: params.Body.OauthEmailDomains, AuthorizationCheckInterval: params.Body.OauthRefreshInterval, } } // create frontend config frontendConfig := &sdk.FrontendConfig{ Interstitial: interstitial, AuthScheme: authScheme, } if authScheme == sdk.Basic { frontendConfig.BasicAuth = &sdk.BasicAuthConfig{Users: authUsers} } if authScheme == sdk.Oauth && oauthCfg != nil { frontendConfig.OauthAuth = oauthCfg } // create config using the global zrokProxyConfigId tags := automation.ZrokShareTags(shrToken) configOpts := &automation.ConfigOptions{ BaseOptions: automation.BaseOptions{ Name: shrToken, Tags: tags, }, ConfigTypeID: zrokProxyConfigId, Data: frontendConfig, } cfgZId, err := ziti.Configs.Create(configOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating config") } // create share service serviceOpts := &automation.ServiceOptions{ BaseOptions: automation.BaseOptions{ Name: shrToken, Tags: tags, }, Configs: []string{cfgZId}, EncryptionRequired: true, } shrZId, err := ziti.Services.Create(serviceOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating share service") } // create bind policy (backend can bind to this service) bindPolicyName := envZId + "-" + shrZId + "-bind" bindPolicyOpts := &automation.ServicePolicyOptions{ BaseOptions: automation.BaseOptions{ Name: bindPolicyName, Tags: tags, }, IdentityRoles: []string{"@" + envZId}, ServiceRoles: []string{"@" + shrZId}, PolicyType: rest_model.DialBindBind, Semantic: rest_model.SemanticAllOf, } _, err = ziti.ServicePolicies.Create(bindPolicyOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating service policy bind") } // create dial policy (frontends can dial this service) // get frontend identities from namespaces var frontendZIds []string for _, selection := range params.Body.NameSelections { ns, err := str.FindNamespaceWithToken(selection.NamespaceToken, trx.(*sqlx.Tx)) if err != nil { return "", nil, errors.Wrapf(err, "error finding namespace with token '%v'", selection.NamespaceToken) } frontends, err := str.FindFrontendsForNamespace(ns.Id, trx.(*sqlx.Tx)) if err != nil { return "", nil, errors.Wrapf(err, "error finding frontends for namespace '%v'", ns.Token) } for _, fe := range frontends { frontendZIds = append(frontendZIds, fe.ZId) } } if len(frontendZIds) > 0 { dialPolicyName := envZId + "-" + shrZId + "-dial" var dialIdentityRoles []string for _, frontendZId := range frontendZIds { dialIdentityRoles = append(dialIdentityRoles, "@"+frontendZId) } dialPolicyOpts := &automation.ServicePolicyOptions{ BaseOptions: automation.BaseOptions{ Name: dialPolicyName, Tags: tags, }, IdentityRoles: dialIdentityRoles, ServiceRoles: []string{"@" + shrZId}, PolicyType: rest_model.DialBindDial, Semantic: rest_model.SemanticAllOf, } _, err = ziti.ServicePolicies.Create(dialPolicyOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating service policy dial") } } // create service edge router policy serpPolicyName := envZId + "-" + shrToken + "-serp" serpPolicyOpts := &automation.ServiceEdgeRouterPolicyOptions{ BaseOptions: automation.BaseOptions{ Name: serpPolicyName, Tags: tags, }, ServiceRoles: []string{"@" + shrZId}, EdgeRouterRoles: []string{"#all"}, Semantic: rest_model.SemanticAllOf, } _, err = ziti.ServiceEdgeRouterPolicies.Create(serpPolicyOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating service edge router policy") } dl.Infof("allocated public resources for share '%v' with service id '%v'", shrToken, shrZId) return shrZId, frontendEndpoints, nil } func (h *shareHandler) checkPrivateShareTokenAvailability(privateShareToken string) error { ziti, err := automation.NewZitiAutomation(cfg.Ziti) if err != nil { return errors.Wrap(err, "error getting ziti automation client") } // verify the service name is available _, err = ziti.Services.GetByName(privateShareToken) if err == nil { return errors.Errorf("service name '%v' is already in use", privateShareToken) } return nil } func (h *shareHandler) allocatePrivateResources(envZId, shrToken string, frontendEndpoints []string, params share.ShareParams, trx interface{}) (string, []string, error) { // get shared automation client ziti, err := automation.NewZitiAutomation(cfg.Ziti) if err != nil { return "", nil, errors.Wrap(err, "error getting ziti automation client") } // prepare auth users var authUsers []*sdk.AuthUserConfig for _, authUser := range params.Body.BasicAuthUsers { authUsers = append(authUsers, &sdk.AuthUserConfig{Username: authUser.Username, Password: authUser.Password}) } // parse auth scheme authScheme, err := sdk.ParseAuthScheme(params.Body.AuthScheme) if err != nil { return "", nil, errors.Wrap(err, "error parsing auth scheme") } // prepare oauth config var oauthCfg *sdk.OauthConfig if authScheme == sdk.Oauth { oauthCfg = &sdk.OauthConfig{ Provider: params.Body.OauthProvider, EmailDomains: params.Body.OauthEmailDomains, AuthorizationCheckInterval: params.Body.OauthRefreshInterval, } } // create frontend config (private shares don't use interstitials) frontendConfig := &sdk.FrontendConfig{ Interstitial: false, AuthScheme: authScheme, } if authScheme == sdk.Basic { frontendConfig.BasicAuth = &sdk.BasicAuthConfig{Users: authUsers} } if authScheme == sdk.Oauth && oauthCfg != nil { frontendConfig.OauthAuth = oauthCfg } // create config using the global zrokProxyConfigId tags := automation.ZrokShareTags(shrToken) configOpts := &automation.ConfigOptions{ BaseOptions: automation.BaseOptions{ Name: shrToken, Tags: tags, }, ConfigTypeID: zrokProxyConfigId, Data: frontendConfig, } cfgZId, err := ziti.Configs.Create(configOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating config") } // create share service serviceOpts := &automation.ServiceOptions{ BaseOptions: automation.BaseOptions{ Name: shrToken, Tags: tags, }, Configs: []string{cfgZId}, EncryptionRequired: true, } shrZId, err := ziti.Services.Create(serviceOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating share service") } // create bind policy (backend can bind to this service) bindPolicyName := envZId + "-" + shrZId + "-bind" bindPolicyOpts := &automation.ServicePolicyOptions{ BaseOptions: automation.BaseOptions{ Name: bindPolicyName, Tags: tags, }, IdentityRoles: []string{"@" + envZId}, ServiceRoles: []string{"@" + shrZId}, PolicyType: rest_model.DialBindBind, Semantic: rest_model.SemanticAllOf, } _, err = ziti.ServicePolicies.Create(bindPolicyOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating service policy bind") } // create service edge router policy serpPolicyName := envZId + "-" + shrToken + "-serp" serpPolicyOpts := &automation.ServiceEdgeRouterPolicyOptions{ BaseOptions: automation.BaseOptions{ Name: serpPolicyName, Tags: tags, }, ServiceRoles: []string{"@" + shrZId}, EdgeRouterRoles: []string{"#all"}, Semantic: rest_model.SemanticAllOf, } _, err = ziti.ServiceEdgeRouterPolicies.Create(serpPolicyOpts) if err != nil { return "", nil, errors.Wrap(err, "error creating service edge router policy") } // note: private shares don't create dial policies here // dial access is granted separately via the access endpoint dl.Infof("allocated private resources for share '%v' with service id '%v'", shrToken, shrZId) return shrZId, frontendEndpoints, nil } func (h *shareHandler) createShareRecord(envId int, shrZId, shrToken string, params share.ShareParams, frontendEndpoints []string, trx interface{}) (int, error) { strShr := &store.Share{ ZId: shrZId, Token: shrToken, ShareMode: params.Body.ShareMode, BackendMode: params.Body.BackendMode, PermissionMode: store.OpenPermissionMode, } // set target as backend proxy endpoint for share12 if params.Body.Target != "" { strShr.BackendProxyEndpoint = ¶ms.Body.Target } // set permission mode if specified if params.Body.PermissionMode != "" { strShr.PermissionMode = store.PermissionMode(params.Body.PermissionMode) } // set frontend endpoint (first one if multiple) if len(frontendEndpoints) > 0 { strShr.FrontendEndpoint = &frontendEndpoints[0] } else if strShr.ShareMode == "private" { // for private shares without frontend endpoints, use the share mode as endpoint strShr.FrontendEndpoint = &strShr.ShareMode } // create the share record shareId, err := str.CreateShare(envId, strShr, trx.(*sqlx.Tx)) if err != nil { return 0, errors.Wrap(err, "error creating share record") } dl.Infof("created share record with id '%v' for share '%v'", shareId, shrToken) return shareId, nil } func (h *shareHandler) processAccessGrants(shareId int, accessGrants []string, permissionMode string, principal *rest_model_zrok.Principal, trx interface{}) error { // only process access grants for closed permission mode if store.PermissionMode(permissionMode) != store.ClosedPermissionMode { return nil } // find account IDs for the access grant email addresses var accessGrantAcctIds []int for _, email := range accessGrants { acct, err := str.FindAccountWithEmail(email, trx.(*sqlx.Tx)) if err != nil { return errors.Wrapf(err, "unable to find account '%v' for share request from '%v'", email, principal.Email) } dl.Debugf("found id '%d' for '%v'", acct.Id, acct.Email) accessGrantAcctIds = append(accessGrantAcctIds, acct.Id) } // create access grants for each account for _, acctId := range accessGrantAcctIds { _, err := str.CreateAccessGrant(shareId, acctId, trx.(*sqlx.Tx)) if err != nil { return errors.Wrapf(err, "error creating access grant for share '%v' and account '%v'", shareId, acctId) } dl.Debugf("created access grant for share '%v' and account '%v'", shareId, acctId) } if len(accessGrantAcctIds) > 0 { dl.Infof("created %d access grants for closed share '%v'", len(accessGrantAcctIds), shareId) } return nil } func (h *shareHandler) processDynamicMappings(shrToken string, nameIds []int, trx *sqlx.Tx) error { // only send updates if dynamic proxy controller is enabled if dPCtrl == nil { dl.Warnf("dynamic proxy controller is nil") return nil } for _, nameId := range nameIds { // find name record to get the name and namespace name, err := str.GetName(nameId, trx) if err != nil { return errors.Wrapf(err, "error finding name with id '%v'", nameId) } // find namespace ns, err := str.GetNamespace(name.NamespaceId, trx) if err != nil { return errors.Wrapf(err, "error finding namespace with id '%v'", name.NamespaceId) } // find dynamic frontends for this namespace frontends, err := str.FindDynamicFrontendsForNamespace(ns.Id, trx) if err != nil { return errors.Wrapf(err, "error finding dynamic frontends for namespace '%v'", ns.Token) } // send mapping updates to each dynamic frontend for _, frontend := range frontends { frontendName := util.NameInNamespace(name.Name, ns.Name) dl.Infof("binding name '%v'", frontendName) if err := dPCtrl.BindFrontendMapping(frontend.Token, frontendName, shrToken, trx); err != nil { dl.Errorf("error binding frontend mapping to frontend '%v': %v", frontend.Token, err) // continue with other frontends rather than failing completely } else { dl.Infof("bound frontend mapping '%v' to dynamic frontend '%v'", frontendName, frontend.Token) } } } return nil }