mirror of
https://github.com/ZwareBear/awx.git
synced 2026-03-20 07:43:35 -05:00
More AWX docs edits
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
## Introduction
|
||||
Starting from Tower 3.3, OAuth 2 will be used as the new means of token-based authentication. Users
|
||||
will be able to manage OAuth 2 tokens as well as applications, a server-side representation of API
|
||||
clients used to generate tokens. With OAuth 2, a user can authenticate by passing a token as part of
|
||||
Starting from Tower 3.3, OAuth2 will be used as the new means of token-based authentication. Users
|
||||
will be able to manage OAuth2 tokens as well as applications, a server-side representation of API
|
||||
clients used to generate tokens. With OAuth2, a user can authenticate by passing a token as part of
|
||||
the HTTP authentication header. The token can be scoped to have more restrictive permissions on top of
|
||||
the base RBAC permissions of the user. Refer to [RFC 6749](https://tools.ietf.org/html/rfc6749) for
|
||||
more details of OAuth 2 specification.
|
||||
more details of OAuth2 specification.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
To get started using OAuth 2 tokens for accessing the browsable API using OAuth 2, this document will walk through the steps of acquiring a token and using it.
|
||||
To get started using OAuth2 tokens for accessing the browsable API using OAuth2, this document will walk through the steps of acquiring a token and using it.
|
||||
|
||||
1. Make an application with `authorization_grant_type` set to 'password'. HTTP POST the following to the `/api/v2/applications/` endpoint (supplying your own `organization-id`):
|
||||
```
|
||||
@@ -51,18 +51,18 @@ curl -H "Authorization: Bearer <token-value>" -X DELETE https://<awx>/api/v2/tok
|
||||
|
||||
## More Information
|
||||
|
||||
#### Managing OAuth 2 Applications and Tokens
|
||||
#### Managing OAuth2 Applications and Tokens
|
||||
|
||||
Applications and tokens can be managed as a top-level resource at `/api/<version>/applications` and
|
||||
`/api/<version>/tokens`. These resources can also be accessed respective to the user at
|
||||
`/api/<version>/users/N/<resource>`. Applications can be created by making a POST to either `api/<version>/applications`
|
||||
or `/api/<version>/users/N/applications`.
|
||||
Applications and tokens can be managed as a top-level resource at `/api/v2/applications` and
|
||||
`/api/v2/tokens`. These resources can also be accessed respective to the user at
|
||||
`/api/v2/users/N/<resource>`. Applications can be created by making a POST to either `api/v2/applications`
|
||||
or `/api/v2/users/N/applications`.
|
||||
|
||||
Each OAuth 2 application represents a specific API client on the server side. For an API client to use the API via an application token,
|
||||
Each OAuth2 application represents a specific API client on the server side. For an API client to use the API via an application token,
|
||||
it must first have an application and issue an access token.
|
||||
|
||||
Individual applications will be accessible via their primary keys:
|
||||
`/api/<version>/applications/<pk>/`. Here is a typical application:
|
||||
`/api/v2/applications/<pk>/`. Here is a typical application:
|
||||
```
|
||||
{
|
||||
"id": 1,
|
||||
@@ -105,8 +105,8 @@ Individual applications will be accessible via their primary keys:
|
||||
```
|
||||
In the above example, `user` is the primary key of the user associated to this application and `name` is
|
||||
a human-readable identifier for the application. The other fields, like `client_id` and
|
||||
`redirect_uris`, are mainly used for OAuth 2 authorization, which will be covered later in the 'Using
|
||||
OAuth 2 Token System' section.
|
||||
`redirect_uris`, are mainly used for OAuth2 authorization, which will be covered later in the 'Using
|
||||
OAuth2 Token System' section.
|
||||
|
||||
Fields `client_id` and `client_secret` are immutable identifiers of applications, and will be
|
||||
generated during creation; Fields `user` and `authorization_grant_type`, on the other hand, are
|
||||
@@ -127,7 +127,7 @@ token scope; or POSTing to `/api/v2/applications/<pk>/tokens/` by providing only
|
||||
the parent application will be automatically linked.
|
||||
|
||||
Individual tokens will be accessible via their primary keys at
|
||||
`/api/<version>/tokens/<pk>/`. Here is a typical token:
|
||||
`/api/v2/tokens/<pk>/`. Here is a typical token:
|
||||
```
|
||||
{
|
||||
"id": 4,
|
||||
@@ -162,7 +162,7 @@ Individual tokens will be accessible via their primary keys at
|
||||
"scope": "read"
|
||||
},
|
||||
```
|
||||
For an OAuth 2 token, the only fully mutable fields are `scope` and `description`. The `application`
|
||||
For an OAuth2 token, the only fully mutable fields are `scope` and `description`. The `application`
|
||||
field is *immutable on update*, and all other fields are totally immutable, and will be auto-populated
|
||||
during creation.
|
||||
* `user` - this field corresponds to the user the token is created for
|
||||
@@ -181,8 +181,8 @@ endpoint. Personal access tokens can be identified by the `application` field b
|
||||
- Other normal users will only be able to see and manipulate their own tokens.
|
||||
> Note: Users can only see the token or refresh-token _value_ at the time of creation ONLY.
|
||||
|
||||
#### Using OAuth 2 Token System for Personal Access Tokens (PAT)
|
||||
The most common usage of OAuth 2 is authenticating users. The `token` field of a token is used
|
||||
#### Using OAuth2 Token System for Personal Access Tokens (PAT)
|
||||
The most common usage of OAuth2 is authenticating users. The `token` field of a token is used
|
||||
as part of the HTTP authentication header, in the format `Authorization: Bearer <token field value>`. This _Bearer_
|
||||
token can be obtained by doing a curl to the `/api/o/token/` endpoint. For example:
|
||||
```
|
||||
@@ -195,17 +195,17 @@ Here is an example of using that PAT to access an API endpoint using `curl`:
|
||||
curl -H "Authorization: Bearer kqHqxfpHGRRBXLNCOXxT5Zt3tpJogn" http://<awx>/api/v2/credentials/
|
||||
```
|
||||
|
||||
According to OAuth 2 specification, users should be able to acquire, revoke and refresh an access
|
||||
According to OAuth2 specification, users should be able to acquire, revoke and refresh an access
|
||||
token. In AWX the equivalent, and easiest, way of doing that is creating a token, deleting
|
||||
a token, and deleting a token quickly followed by creating a new one.
|
||||
|
||||
The specification also provides standard ways of doing this. RFC 6749 elaborates
|
||||
on those topics, but in summary, an OAuth 2 token is officially acquired via authorization using
|
||||
on those topics, but in summary, an OAuth2 token is officially acquired via authorization using
|
||||
authorization information provided by applications (special application fields mentioned above).
|
||||
There are dedicated endpoints for authorization and acquiring tokens. The `token` endpoint
|
||||
is also responsible for token refresh, and token revoke can be done by the dedicated token revoke endpoint.
|
||||
|
||||
In AWX, our OAuth 2 system is built on top of
|
||||
In AWX, our OAuth2 system is built on top of
|
||||
[Django Oauth Toolkit](https://django-oauth-toolkit.readthedocs.io/en/latest/), which provides full
|
||||
support on standard authorization, token revoke and refresh. AWX implements them and puts related
|
||||
endpoints under `/api/o/` endpoint. Detailed examples on the most typical usage of those endpoints
|
||||
@@ -215,9 +215,9 @@ are available as description text of `/api/o/`. See below for information on App
|
||||
|
||||
#### Token Scope Mask Over RBAC System
|
||||
|
||||
The scope of an OAuth 2 token is a space-separated string composed of keywords like 'read' and 'write'.
|
||||
The scope of an OAuth2 token is a space-separated string composed of keywords like 'read' and 'write'.
|
||||
These keywords are configurable and used to specify permission level of the authenticated API client.
|
||||
For the initial OAuth 2 implementation, we use the most simple scope configuration, where the only
|
||||
For the initial OAuth2 implementation, we use the most simple scope configuration, where the only
|
||||
valid scope keywords are 'read' and 'write'.
|
||||
|
||||
Read and write scopes provide a mask layer over the RBAC permission system of AWX. In specific, a
|
||||
@@ -226,7 +226,7 @@ scope gives the authenticated user only read permissions the RBAC system provide
|
||||
|
||||
For example, if a user has admin permission to a job template, he/she can both see and modify, launch
|
||||
and delete the job template if authenticated via session or basic auth. On the other hand, if the user
|
||||
is authenticated using OAuth 2 token, and the related token scope is 'read', the user can only see but
|
||||
is authenticated using OAuth2 token, and the related token scope is 'read', the user can only see but
|
||||
not manipulate or launch the job template, despite being an admin. If the token scope is
|
||||
'write' or 'read write', she can take full advantage of the job template as its admin. Note that 'write'
|
||||
implies 'read' as well.
|
||||
@@ -234,10 +234,10 @@ implies 'read' as well.
|
||||
|
||||
## Application Functions
|
||||
|
||||
This page lists OAuth 2 utility endpoints used for authorization, token refresh and revoke.
|
||||
This page lists OAuth2 utility endpoints used for authorization, token refresh and revoke.
|
||||
Note endpoints other than `/api/o/authorize/` are not meant to be used in browsers and do not
|
||||
support HTTP GET. The endpoints here strictly follow
|
||||
[RFC specs for OAuth 2](https://tools.ietf.org/html/rfc6749), so please use that for detailed
|
||||
[RFC specs for OAuth2](https://tools.ietf.org/html/rfc6749), so please use that for detailed
|
||||
reference. Below are some examples to demonstrate the typical usage of these endpoints in
|
||||
AWX context (note that the AWX net location defaults to `http://localhost:8013` in these examples).
|
||||
|
||||
@@ -392,10 +392,10 @@ at `/api/v2/tokens/`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* All CRUD operations for OAuth 2 applications and tokens should function as described.
|
||||
* RBAC rules applied to OAuth 2 applications and tokens should behave as described.
|
||||
* All CRUD operations for OAuth2 applications and tokens should function as described.
|
||||
* RBAC rules applied to OAuth2 applications and tokens should behave as described.
|
||||
* A default application should be auto-created for each new user.
|
||||
* Incoming requests using unexpired OAuth 2 token correctly in authentication header should be able
|
||||
* Incoming requests using unexpired OAuth2 token correctly in authentication header should be able
|
||||
to successfully authenticate themselves.
|
||||
* Token scope mask over RBAC should work as described.
|
||||
* Tower configuration setting `OAUTH2_PROVIDER` should be configurable and function as described.
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
## Introduction
|
||||
|
||||
Before Tower 3.3, auth token was used as the main authentication method. Starting from Tower 3.3,
|
||||
session-based authentication will take the place as the main authentication method, and auth token
|
||||
Before Tower 3.3, an auth token was used as the main authentication method. Starting from Tower 3.3,
|
||||
session-based authentication will take its place as the main authentication method, and auth token
|
||||
will be replaced by OAuth 2 tokens.
|
||||
|
||||
Session authentication is a safer way of utilizing HTTP(S) cookies:
|
||||
|
||||
Theoretically, the user can provide authentication information, like username and password, as part of the
|
||||
Session authentication is a safer way of utilizing HTTP(S) cookies. Theoretically, the user can provide authentication information, like username and password, as part of the
|
||||
`Cookie` header, but this method is vulnerable to cookie hijacks, where crackers can see and steal user
|
||||
information from cookie payload.
|
||||
information from the cookie payload.
|
||||
|
||||
Session authentication, on the other hand, sets a single `session_id` cookie. The session_id
|
||||
Session authentication, on the other hand, sets a single `session_id` cookie. The `session_id`
|
||||
is *a random string which will be mapped to user authentication informations by server*. Crackers who
|
||||
hijack cookies will only get the session_id itself, which does not imply any critical user info, is valid only for
|
||||
hijack cookies will only get the `session_id` itself, which does not imply any critical user info, is valid only for
|
||||
a limited time, and can be revoked at any time.
|
||||
|
||||
> Note: The CSRF token will by default allow HTTP. To increase security, the `CSRF_COOKIE_SECURE` setting should
|
||||
> Note: The CSRF token will by default allow HTTP. To increase security, the `CSRF_COOKIE_SECURE` setting should
|
||||
be set to False.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
In session authentication, users log in using the `/api/login/` endpoint. A GET to `/api/login/` displays the
|
||||
log in page of API browser:
|
||||
login page of API browser:
|
||||
|
||||

|
||||
|
||||
Users should enter correct username and password before clicking on 'LOG IN' button, which fires a POST
|
||||
Users should enter correct username and password before clicking on the 'LOG IN' button, which fires a POST
|
||||
to `/api/login/` to actually log the user in. The return code of a successful login is 302, meaning upon
|
||||
successful login, the browser will be redirected, the redirected destination is determined by `next` form
|
||||
successful login, the browser will be redirected; the redirected destination is determined by the `next` form
|
||||
item described below.
|
||||
|
||||
It should be noted that POST body of `/api/login/` is *not* in JSON, but HTTP form format. 4 items should
|
||||
It should be noted that the POST body of `/api/login/` is *not* in JSON, but in HTTP form format. Four items should
|
||||
be provided in the form:
|
||||
* `username`: The username of the user trying to log in.
|
||||
* `password`: The password of the user trying to log in.
|
||||
* `next`: The path of the redirect destination, in API browser `"/api/"` is used.
|
||||
* `csrfmiddlewaretoken`: The CSRF token, usually populated by using Django template `{% csrf_token %}`.
|
||||
|
||||
The session_id is provided as a return `Set-Cookie` header. Here is a typical one:
|
||||
The `session_id` is provided as a return `Set-Cookie` header. Here is a typical one:
|
||||
```
|
||||
Set-Cookie: sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; expires=Tue, 21-Nov-2017 16:33:13 GMT; httponly; Max-Age=1209600; Path=/
|
||||
```
|
||||
@@ -47,40 +46,42 @@ session cookie value, expiration date, duration, etc.
|
||||
|
||||
The duration of the cookie is configurable by Tower Configuration setting `SESSION_COOKIE_AGE` under
|
||||
category `authentication`. It is an integer denoting the number of seconds the session cookie should
|
||||
live. The default session cookie age is 2 weeks.
|
||||
live. The default session cookie age is two weeks.
|
||||
|
||||
After a valid session is acquired, a client should provide the session_id as a cookie for subsequent requests
|
||||
After a valid session is acquired, a client should provide the `session_id` as a cookie for subsequent requests
|
||||
in order to be authenticated. For example:
|
||||
```
|
||||
Cookie: sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; ...
|
||||
```
|
||||
|
||||
User should use `/api/logout/` endpoint to log out. In API browser, a logged in user can do that by
|
||||
simply clicking logout button on the nav bar. Under the hood the click issues a GET to '/api/logout/',
|
||||
Upon success, server will invalidate current session and the response header will indicate client
|
||||
to delete the session cookie. User should no longer try using this invalid session.
|
||||
User should use the `/api/logout/` endpoint to log out. In the API browser, a logged-in user can do that by
|
||||
simply clicking logout button on the nav bar. Under the hood, the click issues a GET to `/api/logout/`.
|
||||
Upon success, the server will invalidate the current session and the response header will indicate for the client
|
||||
to delete the session cookie. The user should no longer try using this invalid session.
|
||||
|
||||
The duration of a session is constant. However, user can extend the expiration date of a valid session
|
||||
The duration of a session is constant. However, a user can extend the expiration date of a valid session
|
||||
by performing session acquire with the session provided.
|
||||
|
||||
A Tower configuration setting, `SESSIONS_PER_USER` under category `authentication`, is used to set the
|
||||
maximum number of valid sessions a user can have at the same time. For example, if `SESSIONS_PER_USER`
|
||||
is set to 3 and the same user is logged in from 5 different places, the earliest 2 sessions created will be invalidated. Tower will try
|
||||
is set to three and the same user is logged in from five different places, the earliest two sessions created will be invalidated. Tower will try
|
||||
broadcasting, via websocket, to all available clients. The websocket message body will contain a list of
|
||||
invalidated sessions. If a client finds its session in that list, it should try logging out.
|
||||
|
||||
Unlike tokens, sessions are meant to be short-lived and UI-only, therefore whenever a user's password
|
||||
Unlike tokens, sessions are meant to be short-lived and UI-only; therefore, whenever a user's password
|
||||
is updated, all sessions she owned will be invalidated and deleted.
|
||||
|
||||
|
||||
## Acceptance Criteria
|
||||
* User should be able to log in via `/api/login/` endpoint by correctly providing all necessary fields.
|
||||
* Logged in users should be able to authenticate themselves by providing correct session auth info.
|
||||
* Logged in users should be able to log out via `/api/logout/`.
|
||||
|
||||
* Users should be able to log in via the `/api/login/` endpoint by correctly providing all necessary fields.
|
||||
* Logged-in users should be able to authenticate themselves by providing correct session auth info.
|
||||
* Logged-in users should be able to log out via `/api/logout/`.
|
||||
* The duration of a session cookie should be configurable by `SESSION_COOKIE_AGE`.
|
||||
* The maximum number of concurrent login for one user should be configurable by `SESSIONS_PER_USER`,
|
||||
and over-limit user sessions should be warned by websocket.
|
||||
* When a user's password is changed, all her sessions should be invalidated and deleted.
|
||||
* User should not be able to authenticate by HTTPS(S) request nor websocket connect using invalid
|
||||
* User should not be able to authenticate by HTTPS(S) request nor websocket connection using invalid
|
||||
sessions.
|
||||
* No existing behavior, like job run, inventory update or callback receiver, should be affected
|
||||
* No existing behavior, like job runs, inventory updates or callback receiver, should be affected
|
||||
by session auth.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# TACACS+
|
||||
[Terminal Access Controller Access-Control System Plus (TACACS+)](https://en.wikipedia.org/wiki/TACACS) is a protocol developed by Cisco to handle remote authentication and related services for networked access control through a centralized server. In specific, TACACS+ provides authentication, authorization and accounting (AAA) services. Ansible Tower currently utilizes its authentication service.
|
||||
|
||||
TACACS+ is configured by Tower configuration and is available under `/api/<version #>/settings/tacacsplus/`. Here is a typical configuration with every configurable field included:
|
||||
TACACS+ is configured by Tower configuration and is available under `/api/v2/settings/tacacsplus/`. Here is a typical configuration with every configurable field included:
|
||||
```
|
||||
{
|
||||
"TACACSPLUS_HOST": "127.0.0.1",
|
||||
@@ -11,7 +11,7 @@ TACACS+ is configured by Tower configuration and is available under `/api/<versi
|
||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii"
|
||||
}
|
||||
```
|
||||
Below explains each field:
|
||||
Each field is explained below:
|
||||
|
||||
| Field Name | Field Value Type | Field Value Default | Description |
|
||||
|------------------------------|---------------------|---------------------|--------------------------------------------------------------------|
|
||||
@@ -19,28 +19,31 @@ Below explains each field:
|
||||
| `TACACSPLUS_PORT` | Integer | 49 | Port number of TACACS+ server. |
|
||||
| `TACACSPLUS_SECRET` | String | '' (empty string) | Shared secret for authenticating to TACACS+ server. |
|
||||
| `TACACSPLUS_SESSION_TIMEOUT` | Integer | 5 | TACACS+ session timeout value in seconds. |
|
||||
| `TACACSPLUS_AUTH_PROTOCOL` | String with choices | 'ascii' | The authentication protocol used by TACACS+ client. Choices are `ascii` and `pap` |
|
||||
| `TACACSPLUS_AUTH_PROTOCOL` | String with choices | 'ascii' | The authentication protocol used by TACACS+ client (choices are `ascii` and `pap`). |
|
||||
|
||||
Under the hood, Tower uses [open-source TACACS+ python client](https://github.com/ansible/tacacs_plus) to communicate with the remote TACACS+ server. During authentication, Tower passes username and password to TACACS+ client, which packs up auth information and send to TACACS+ server. Based on what the server returns, Tower will invalidate login attempt if authentication fails. If authentication passes, Tower will create a user if she does not exist in database, and log the user in.
|
||||
Under the hood, Tower uses [open-source TACACS+ python client](https://github.com/ansible/tacacs_plus) to communicate with the remote TACACS+ server. During authentication, Tower passes username and password to TACACS+ client, which packs up auth information and sends it to the TACACS+ server. Based on what the server returns, Tower will invalidate login attempt if authentication fails. If authentication passes, Tower will create a user if she does not exist in database, and log the user in.
|
||||
|
||||
## Test environment setup
|
||||
## Test Environment Setup
|
||||
|
||||
The suggested TACACS+ server for testing is [shrubbery TACACS+ daemon](http://www.shrubbery.net/tac_plus/). It is supposed to run on a centos machine. A verified candidate is centos 6.3 AMI in AWS EC2 Community AMIs (search for 'Centos 6.3 x86_64 HVM - Minimal with cloud-init aws-cfn-bootstrap and ec2-api-tools'). Note it is required to keep TCP port 49 open, since it's the default port used by TACACS+ daemon.
|
||||
The suggested TACACS+ server for testing is [shrubbery TACACS+ daemon](http://www.shrubbery.net/tac_plus/). It is supposed to run on a CentOS machine. A verified candidate is CentOS 6.3 AMI in AWS EC2 Community AMIs (search for `CentOS 6.3 x86_64 HVM - Minimal with cloud-init aws-cfn-bootstrap and ec2-api-tools`). Note that it is required to keep TCP port 49 open, since it's the default port used by the TACACS+ daemon.
|
||||
|
||||
We provide [a playbook](https://github.com/jangsutsr/ansible-role-tacacs) to install a working TACACS+ server. Here is a typical test setup using the provided playbook.
|
||||
1. In AWS EC2, spawn the centos 6 machine.
|
||||
We provide [a playbook](https://github.com/jangsutsr/ansible-role-tacacs) to install a working TACACS+ server. Here is a typical test setup using the provided playbook:
|
||||
|
||||
1. In AWS EC2, spawn the CentOS 6 machine.
|
||||
2. In Tower, create a test project using the stand-alone playbook inventory.
|
||||
3. In Tower, create a test inventory with the only host to be the spawned centos machine.
|
||||
4. In Tower, create and run a job template using the created project and inventory with parameters setup as below.
|
||||
3. In Tower, create a test inventory with the only host to be the spawned CentOS machine.
|
||||
4. In Tower, create and run a job template using the created project and inventory with parameters setup as below:
|
||||
|
||||

|
||||
|
||||
The playbook creates a user named 'tower' with ascii password default to 'login' and modifiable by extra_var `ascii_password` and pap password default to 'papme' and modifiable by extra_var `pap_password`. In order to configure TACACS+ server to meet custom test needs, we need to modify server-side file `/etc/tac_plus.conf` and `sudo service tac_plus restart` to restart the daemon. Details on how to modify config file can be found [here](http://manpages.ubuntu.com/manpages/xenial/man5/tac_plus.conf.5.html).
|
||||
The playbook creates a user named 'tower' with ascii password default to 'login' and modifiable by `extra_var` `ascii_password` and pap password default to 'papme' and modifiable by `extra_var` `pap_password`. In order to configure TACACS+ server to meet custom test needs, we need to modify server-side file `/etc/tac_plus.conf` and `sudo service tac_plus restart` to restart the daemon. Details on how to modify config file can be found [here](http://manpages.ubuntu.com/manpages/xenial/man5/tac_plus.conf.5.html).
|
||||
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
## Acceptance criteria
|
||||
* All specified Tower configuration fields should be shown and configurable as documented.
|
||||
* User defined by TACACS+ server should be able to log in Tower.
|
||||
* User not defined by TACACS+ server should not be able to log in Tower via TACACS+.
|
||||
* A user existing in TACACS+ server but not in Tower should be created after the first success log in.
|
||||
* TACACS+ backend should stop authentication attempt after configured timeout and should not block the authentication pipeline in any case.
|
||||
* A user defined by the TACACS+ server should be able to log into Tower.
|
||||
* User not defined by TACACS+ server should not be able to log into Tower via TACACS+.
|
||||
* A user existing in TACACS+ server but not in Tower should be created after the first successful log in.
|
||||
* TACACS+ backend should stop an authentication attempt after configured timeout and should not block the authentication pipeline in any case.
|
||||
* If exceptions occur on TACACS+ server side, the exception details should be logged in Tower, and Tower should not authenticate that user via TACACS+.
|
||||
|
||||
Reference in New Issue
Block a user