mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'gnet-oauth'
This commit is contained in:
commit
3e657357e5
@ -9,7 +9,7 @@ app_mode = production
|
||||
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
|
||||
instance_name = ${HOSTNAME}
|
||||
|
||||
#################################### Paths ####################################
|
||||
#################################### Paths ###############################
|
||||
[paths]
|
||||
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
|
||||
#
|
||||
@ -23,7 +23,7 @@ logs = data/log
|
||||
#
|
||||
plugins = data/plugins
|
||||
|
||||
#################################### Server ####################################
|
||||
#################################### Server ##############################
|
||||
[server]
|
||||
# Protocol (http or https)
|
||||
protocol = http
|
||||
@ -57,7 +57,7 @@ enable_gzip = false
|
||||
cert_file =
|
||||
cert_key =
|
||||
|
||||
#################################### Database ####################################
|
||||
#################################### Database ############################
|
||||
[database]
|
||||
# You can configure the database connection by specifying type, host, name, user and password
|
||||
# as seperate properties or as on string using the url propertie.
|
||||
@ -84,7 +84,7 @@ server_cert_name =
|
||||
# For "sqlite3" only, path relative to data_path setting
|
||||
path = grafana.db
|
||||
|
||||
#################################### Session ####################################
|
||||
#################################### Session #############################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||
provider = file
|
||||
@ -112,7 +112,7 @@ cookie_secure = false
|
||||
session_life_time = 86400
|
||||
gc_interval_time = 86400
|
||||
|
||||
#################################### Analytics ####################################
|
||||
#################################### Analytics ###########################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
# No ip addresses are being tracked, only simple counters to track
|
||||
@ -133,7 +133,7 @@ google_analytics_ua_id =
|
||||
# Google Tag Manager ID, only enabled if you specify an id here
|
||||
google_tag_manager_id =
|
||||
|
||||
#################################### Security ####################################
|
||||
#################################### Security ############################
|
||||
[security]
|
||||
# default admin user, created on startup
|
||||
admin_user = admin
|
||||
@ -193,7 +193,7 @@ default_theme = dark
|
||||
# Allow users to sign in using username and password
|
||||
allow_user_pass_login = true
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
enabled = false
|
||||
@ -204,7 +204,7 @@ org_name = Main Org.
|
||||
# specify role for unauthenticated users
|
||||
org_role = Viewer
|
||||
|
||||
#################################### Github Auth ##########################
|
||||
#################################### Github Auth #########################
|
||||
[auth.github]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -217,7 +217,7 @@ api_url = https://api.github.com/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
|
||||
#################################### Google Auth ##########################
|
||||
#################################### Google Auth #########################
|
||||
[auth.google]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -229,7 +229,16 @@ token_url = https://accounts.google.com/o/oauth2/token
|
||||
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
||||
allowed_domains =
|
||||
|
||||
#################################### Generic OAuth ##########################
|
||||
#################################### Grafana.net Auth ####################
|
||||
[auth.grafananet]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
client_id = some_id
|
||||
client_secret = some_secret
|
||||
scopes = user:email
|
||||
allowed_organizations =
|
||||
|
||||
#################################### Generic OAuth #######################
|
||||
[auth.generic_oauth]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -253,12 +262,12 @@ header_name = X-WEBAUTH-USER
|
||||
header_property = username
|
||||
auto_sign_up = true
|
||||
|
||||
#################################### Auth LDAP ##########################
|
||||
#################################### Auth LDAP ###########################
|
||||
[auth.ldap]
|
||||
enabled = false
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
|
||||
#################################### SMTP / Emailing ##########################
|
||||
#################################### SMTP / Emailing #####################
|
||||
[smtp]
|
||||
enabled = false
|
||||
host = localhost:25
|
||||
@ -273,9 +282,6 @@ from_address = admin@grafana.localhost
|
||||
welcome_email_on_sign_up = false
|
||||
templates_pattern = emails/*.html
|
||||
|
||||
[tmp.files]
|
||||
rendered_image_ttl_days = 14
|
||||
|
||||
#################################### Logging ##########################
|
||||
[log]
|
||||
# Either "console", "file", "syslog". Default is console and file
|
||||
@ -331,18 +337,18 @@ facility =
|
||||
tag =
|
||||
|
||||
|
||||
#################################### AMQP Event Publisher ##########################
|
||||
#################################### AMQP Event Publisher ################
|
||||
[event_publisher]
|
||||
enabled = false
|
||||
rabbitmq_url = amqp://localhost/
|
||||
exchange = grafana_events
|
||||
|
||||
#################################### Dashboard JSON files ##########################
|
||||
#################################### Dashboard JSON files ################
|
||||
[dashboards.json]
|
||||
enabled = false
|
||||
path = /var/lib/grafana/dashboards
|
||||
|
||||
#################################### Usage Quotas ##########################
|
||||
#################################### Usage Quotas ########################
|
||||
[quota]
|
||||
enabled = false
|
||||
|
||||
@ -377,7 +383,7 @@ global_api_key = -1
|
||||
# global limit on number of logged in users.
|
||||
global_session = -1
|
||||
|
||||
#################################### Alerting ######################################
|
||||
#################################### Alerting ############################
|
||||
# docs about alerting can be found in /docs/sources/alerting/
|
||||
# __.-/|
|
||||
# \`o_O'
|
||||
@ -396,7 +402,7 @@ global_session = -1
|
||||
[alerting]
|
||||
enabled = true
|
||||
|
||||
#################################### Internal Grafana Metrics ##########################
|
||||
#################################### Internal Grafana Metrics ############
|
||||
# Metrics available at HTTP API Url /api/metrics
|
||||
[metrics]
|
||||
enabled = true
|
||||
@ -411,7 +417,7 @@ prefix = prod.grafana.%(instance_name)s.
|
||||
[grafana_net]
|
||||
url = https://grafana.net
|
||||
|
||||
#################################### External image storage ##########################
|
||||
#################################### External Image Storage ##############
|
||||
[external_image_storage]
|
||||
# You can choose between (s3, webdav)
|
||||
provider = s3
|
||||
|
@ -116,7 +116,7 @@
|
||||
# in some UI views to notify that grafana or plugin update exists
|
||||
# This option does not cause any auto updates, nor send any information
|
||||
# only a GET request to http://grafana.net to get latest versions
|
||||
check_for_updates = true
|
||||
;check_for_updates = true
|
||||
|
||||
# Google Analytics universal tracking code, only enabled if you specify an id here
|
||||
;google_analytics_ua_id =
|
||||
@ -224,6 +224,15 @@ check_for_updates = true
|
||||
;team_ids =
|
||||
;allowed_organizations =
|
||||
|
||||
#################################### Grafana.net Auth ####################
|
||||
[auth.grafananet]
|
||||
;enabled = false
|
||||
;allow_sign_up = false
|
||||
;client_id = some_id
|
||||
;client_secret = some_secret
|
||||
;scopes = user:email
|
||||
;allowed_organizations =
|
||||
|
||||
#################################### Auth Proxy ##########################
|
||||
[auth.proxy]
|
||||
;enabled = false
|
||||
|
@ -25,10 +25,12 @@ func LoginView(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||
viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
|
||||
viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
|
||||
enabledOAuths := make(map[string]interface{})
|
||||
for key, oauth := range setting.OAuthService.OAuthInfos {
|
||||
enabledOAuths[key] = map[string]string{"name": oauth.Name}
|
||||
}
|
||||
|
||||
viewData.Settings["oauth"] = enabledOAuths
|
||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
||||
|
@ -82,10 +82,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: userInfo.Email,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
Login: userInfo.Email,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
DefaultOrgRole: userInfo.Role,
|
||||
}
|
||||
|
||||
if err = bus.Dispatch(&cmd); err != nil {
|
||||
|
@ -7,4 +7,5 @@ const (
|
||||
GOOGLE
|
||||
TWITTER
|
||||
GENERIC
|
||||
GRAFANANET
|
||||
)
|
||||
|
@ -44,15 +44,16 @@ func (u *User) NameOrFallback() string {
|
||||
// COMMANDS
|
||||
|
||||
type CreateUserCommand struct {
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
SkipOrgSetup bool
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
SkipOrgSetup bool
|
||||
DefaultOrgRole string
|
||||
|
||||
Result User
|
||||
}
|
||||
|
@ -128,7 +128,11 @@ func CreateUser(cmd *m.CreateUserCommand) error {
|
||||
}
|
||||
|
||||
if setting.AutoAssignOrg && !user.IsAdmin {
|
||||
orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
|
||||
if len(cmd.DefaultOrgRole) > 0 {
|
||||
orgUser.Role = m.RoleType(cmd.DefaultOrgRole)
|
||||
} else {
|
||||
orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = sess.Insert(&orgUser); err != nil {
|
||||
|
@ -8,12 +8,11 @@ type OAuthInfo struct {
|
||||
AllowedDomains []string
|
||||
ApiUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
GitHub, Google, Twitter, Generic bool
|
||||
OAuthInfos map[string]*OAuthInfo
|
||||
OAuthProviderName string
|
||||
OAuthInfos map[string]*OAuthInfo
|
||||
}
|
||||
|
||||
var OAuthService *OAuther
|
||||
|
114
pkg/social/grafananet_oauth.go
Normal file
114
pkg/social/grafananet_oauth.go
Normal file
@ -0,0 +1,114 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type SocialGrafanaNet struct {
|
||||
*oauth2.Config
|
||||
url string
|
||||
allowedOrganizations []string
|
||||
allowSignup bool
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) Type() int {
|
||||
return int(models.GRAFANANET)
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsEmailAllowed(email string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsSignupAllowed() bool {
|
||||
return s.allowSignup
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
|
||||
if len(s.allowedOrganizations) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
organizations, err := s.FetchOrganizations(client)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, allowedOrganization := range s.allowedOrganizations {
|
||||
for _, organization := range organizations {
|
||||
if organization == allowedOrganization {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) {
|
||||
type Record struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs")
|
||||
r, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var records []Record
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var logins = make([]string, len(records))
|
||||
for i, record := range records {
|
||||
logins[i] = record.Login
|
||||
}
|
||||
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
var err error
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.url + "/api/oauth2/user")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Identity: strconv.Itoa(data.Id),
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
Role: data.Role,
|
||||
}
|
||||
|
||||
if !s.IsOrganizationMember(client) {
|
||||
return nil, ErrMissingOrganizationMembership
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
@ -15,6 +15,7 @@ type BasicUserInfo struct {
|
||||
Email string
|
||||
Login string
|
||||
Company string
|
||||
Role string
|
||||
}
|
||||
|
||||
type SocialConnector interface {
|
||||
@ -36,7 +37,7 @@ func NewOAuthService() {
|
||||
setting.OAuthService = &setting.OAuther{}
|
||||
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
||||
|
||||
allOauthes := []string{"github", "google", "generic_oauth"}
|
||||
allOauthes := []string{"github", "google", "generic_oauth", "grafananet"}
|
||||
|
||||
for _, name := range allOauthes {
|
||||
sec := setting.Cfg.Section("auth." + name)
|
||||
@ -50,6 +51,7 @@ func NewOAuthService() {
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
AllowedDomains: sec.Key("allowed_domains").Strings(" "),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
@ -70,22 +72,18 @@ func NewOAuthService() {
|
||||
|
||||
// GitHub.
|
||||
if name == "github" {
|
||||
setting.OAuthService.GitHub = true
|
||||
teamIds := sec.Key("team_ids").Ints(",")
|
||||
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
|
||||
SocialMap["github"] = &SocialGithub{
|
||||
Config: &config,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: allowedOrganizations,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
|
||||
// Google.
|
||||
if name == "google" {
|
||||
setting.OAuthService.Google = true
|
||||
SocialMap["google"] = &SocialGoogle{
|
||||
Config: &config, allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
@ -95,17 +93,33 @@ func NewOAuthService() {
|
||||
|
||||
// Generic - Uses the same scheme as Github.
|
||||
if name == "generic_oauth" {
|
||||
setting.OAuthService.Generic = true
|
||||
setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
|
||||
teamIds := sec.Key("team_ids").Ints(",")
|
||||
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
|
||||
SocialMap["generic_oauth"] = &GenericOAuth{
|
||||
Config: &config,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: allowedOrganizations,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
|
||||
if name == "grafananet" {
|
||||
config := oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize",
|
||||
TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
SocialMap["grafananet"] = &SocialGrafanaNet{
|
||||
Config: &config,
|
||||
url: setting.GrafanaNetUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, coreModule, config) {
|
||||
function (angular, _, coreModule, config) {
|
||||
'use strict';
|
||||
|
||||
var failCodes = {
|
||||
"1000": "Required Github team membership not fulfilled",
|
||||
"1001": "Required Github organization membership not fulfilled",
|
||||
"1000": "Required team membership not fulfilled",
|
||||
"1001": "Required organization membership not fulfilled",
|
||||
"1002": "Required email domain not fulfilled",
|
||||
};
|
||||
|
||||
@ -21,12 +22,10 @@ function (angular, coreModule, config) {
|
||||
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
|
||||
$scope.oauth = config.oauth;
|
||||
$scope.oauthEnabled = _.keys(config.oauth).length > 0;
|
||||
|
||||
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
||||
$scope.genericOAuthEnabled = config.genericOAuthEnabled;
|
||||
$scope.oauthProviderName = config.oauthProviderName;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
$scope.loginHint = config.loginHint;
|
||||
|
||||
|
@ -51,18 +51,21 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="login-oauth text-center" ng-show="oauthEnabled">
|
||||
<a class="btn btn-large btn-google" href="login/google" target="_self" ng-if="googleAuthEnabled">
|
||||
<a class="btn btn-large btn-google" href="login/google" target="_self" ng-if="oauth.google">
|
||||
<i class="fa fa-google"></i>
|
||||
with Google
|
||||
</a>
|
||||
<a class="btn btn-large btn-github" href="login/github" target="_self" ng-if="githubAuthEnabled">
|
||||
<a class="btn btn-large btn-github" href="login/github" target="_self" ng-if="oauth.github">
|
||||
<i class="fa fa-github"></i>
|
||||
with Github
|
||||
</a>
|
||||
<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="genericOAuthEnabled">
|
||||
<a class="btn btn-large btn-grafana-net" href="login/grafananet" target="_self" ng-if="oauth.grafananet">
|
||||
with <span>Grafana.net</span>
|
||||
</a>
|
||||
<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="oauth.generic_oauth">
|
||||
<i class="fa fa-gear"></i>
|
||||
with {{oauthProviderName || "OAuth 2"}}
|
||||
</a>
|
||||
with {{oauth.generic_oauth.name}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -111,7 +111,7 @@
|
||||
font-size: $font-size-sm;
|
||||
padding-right: 7rem;
|
||||
background: url(../img/grafana_net_logo.svg);
|
||||
background-size: 6.5rem 3rem;
|
||||
background-size: 6.5rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right;
|
||||
position: relative;
|
||||
|
@ -112,6 +112,19 @@
|
||||
background: #555;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-grafana-net {
|
||||
background: url(../img/grafana_net_logo.svg);
|
||||
background-size: 10rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 35%;
|
||||
overflow: hidden;
|
||||
padding-right: 10.5rem;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-recovery {
|
||||
@ -157,7 +170,7 @@
|
||||
.invite-box {
|
||||
text-align: center;
|
||||
border: 1px solid $tight-form-func-bg;
|
||||
background-color: $panel-bg;
|
||||
background-color: $panel-bg;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
Loading…
Reference in New Issue
Block a user